From 0f61ec9e987b9a93d9072d2cb7dbf39017c027e7 Mon Sep 17 00:00:00 2001 From: Rico Sonntag Date: Fri, 24 Nov 2023 12:07:36 +0100 Subject: [PATCH] Rework tree creation code, consolidate with descendants chart --- module.php | 2 +- package.json | 6 +- resources/css/pedigree-chart.css | 7 + resources/css/svg.css | 3 + .../js/modules/chart/elbow/horizontal.js | 32 - resources/js/modules/chart/elbow/vertical.js | 32 - .../js/modules/{ => custom}/configuration.js | 6 +- resources/js/modules/custom/data.js | 84 ++ .../js/modules/{chart => custom}/hierarchy.js | 76 +- resources/js/modules/custom/tree.js | 131 +++ resources/js/modules/data.js | 66 -- resources/js/modules/index.js | 15 +- resources/js/modules/{ => lib}/chart.js | 89 +- resources/js/modules/{ => lib}/chart/box.js | 25 +- .../js/modules/{ => lib}/chart/box/image.js | 59 +- .../js/modules/{ => lib}/chart/box/text.js | 52 +- .../{ => lib}/chart/orientation-collection.js | 2 +- .../orientation/orientation-bottomTop.js | 19 +- .../orientation/orientation-leftRight.js | 27 +- .../orientation/orientation-rightLeft.js | 27 +- .../orientation/orientation-topBottom.js | 19 +- .../chart/orientation/orientation.js | 56 +- .../js/modules/{ => lib}/chart/overlay.js | 2 +- resources/js/modules/{ => lib}/chart/svg.js | 12 +- .../js/modules/{ => lib}/chart/svg/defs.js | 2 +- .../{ => lib}/chart/svg/export-factory.js | 2 +- .../js/modules/{ => lib}/chart/svg/export.js | 4 +- .../modules/{ => lib}/chart/svg/export/png.js | 4 +- .../modules/{ => lib}/chart/svg/export/svg.js | 22 +- .../js/modules/{ => lib}/chart/svg/zoom.js | 14 +- .../js/modules/lib/chart/text/measure.js | 34 + .../js/modules/{ => lib}/chart/update.js | 4 +- .../js/modules/{ => lib}/common/dataUrl.js | 18 +- resources/js/modules/{ => lib}/common/dpi.js | 2 +- resources/js/modules/{ => lib}/constants.js | 11 +- resources/js/modules/{ => lib}/d3.js | 4 +- resources/js/modules/{ => lib}/storage.js | 12 +- resources/js/modules/lib/tree/date.js | 203 ++++ .../js/modules/lib/tree/elbow/horizontal.js | 143 +++ .../js/modules/lib/tree/elbow/vertical.js | 141 +++ resources/js/modules/lib/tree/link-drawer.js | 130 +++ resources/js/modules/lib/tree/name.js | 421 ++++++++ resources/js/modules/lib/tree/node-drawer.js | 263 +++++ resources/js/modules/tree.js | 908 ------------------ resources/js/pedigree-chart.min.js | 2 +- resources/views/layouts/ajax.phtml | 2 +- resources/views/modules/charts/chart.phtml | 2 +- .../views/modules/pedigree-chart/chart.phtml | 2 +- .../views/modules/pedigree-chart/page.phtml | 2 +- rollup.config.js | 4 +- src/Configuration.php | 2 +- src/Facade/DataFacade.php | 244 +++++ src/Model/Node.php | 83 ++ src/Model/NodeData.php | 401 ++++++++ src/Module.php | 114 +-- src/Processor/DateProcessor.php | 180 ++++ src/Processor/ImageProcessor.php | 92 ++ src/Processor/NameProcessor.php | 255 +++++ src/Traits/IndividualTrait.php | 337 ------- src/Traits/ModuleChartTrait.php | 2 +- src/Traits/ModuleCustomTrait.php | 2 +- test/MultiByteTest.php | 2 +- 62 files changed, 3147 insertions(+), 1772 deletions(-) delete mode 100644 resources/js/modules/chart/elbow/horizontal.js delete mode 100644 resources/js/modules/chart/elbow/vertical.js rename resources/js/modules/{ => custom}/configuration.js (95%) create mode 100644 resources/js/modules/custom/data.js rename resources/js/modules/{chart => custom}/hierarchy.js (57%) create mode 100644 resources/js/modules/custom/tree.js delete mode 100644 resources/js/modules/data.js rename resources/js/modules/{ => lib}/chart.js (66%) rename resources/js/modules/{ => lib}/chart/box.js (82%) rename resources/js/modules/{ => lib}/chart/box/image.js (78%) rename resources/js/modules/{ => lib}/chart/box/text.js (66%) rename resources/js/modules/{ => lib}/chart/orientation-collection.js (96%) rename resources/js/modules/{ => lib}/chart/orientation/orientation-bottomTop.js (72%) rename resources/js/modules/{ => lib}/chart/orientation/orientation-leftRight.js (65%) rename resources/js/modules/{ => lib}/chart/orientation/orientation-rightLeft.js (65%) rename resources/js/modules/{ => lib}/chart/orientation/orientation-topBottom.js (72%) rename resources/js/modules/{ => lib}/chart/orientation/orientation.js (66%) rename resources/js/modules/{ => lib}/chart/overlay.js (97%) rename resources/js/modules/{ => lib}/chart/svg.js (93%) rename resources/js/modules/{ => lib}/chart/svg/defs.js (92%) rename resources/js/modules/{ => lib}/chart/svg/export-factory.js (94%) rename resources/js/modules/{ => lib}/chart/svg/export.js (91%) rename resources/js/modules/{ => lib}/chart/svg/export/png.js (97%) rename resources/js/modules/{ => lib}/chart/svg/export/svg.js (78%) rename resources/js/modules/{ => lib}/chart/svg/zoom.js (84%) create mode 100644 resources/js/modules/lib/chart/text/measure.js rename resources/js/modules/{ => lib}/chart/update.js (95%) rename resources/js/modules/{ => lib}/common/dataUrl.js (57%) rename resources/js/modules/{ => lib}/common/dpi.js (89%) rename resources/js/modules/{ => lib}/constants.js (75%) rename resources/js/modules/{ => lib}/d3.js (86%) rename resources/js/modules/{ => lib}/storage.js (84%) create mode 100644 resources/js/modules/lib/tree/date.js create mode 100644 resources/js/modules/lib/tree/elbow/horizontal.js create mode 100644 resources/js/modules/lib/tree/elbow/vertical.js create mode 100644 resources/js/modules/lib/tree/link-drawer.js create mode 100644 resources/js/modules/lib/tree/name.js create mode 100644 resources/js/modules/lib/tree/node-drawer.js delete mode 100644 resources/js/modules/tree.js create mode 100644 src/Facade/DataFacade.php create mode 100644 src/Model/Node.php create mode 100644 src/Model/NodeData.php create mode 100644 src/Processor/DateProcessor.php create mode 100644 src/Processor/ImageProcessor.php create mode 100644 src/Processor/NameProcessor.php delete mode 100644 src/Traits/IndividualTrait.php diff --git a/module.php b/module.php index 97e70c0..2980954 100644 --- a/module.php +++ b/module.php @@ -4,7 +4,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ declare(strict_types=1); diff --git a/package.json b/package.json index 83e16c7..0fa6fe7 100644 --- a/package.json +++ b/package.json @@ -21,14 +21,14 @@ "watch": "rollup --watch --config rollup.config.js" }, "devDependencies": { - "@rollup/plugin-node-resolve": "^15.2.1", - "@rollup/plugin-terser": "^0.4.3", + "@rollup/plugin-node-resolve": "^15.2.3", + "@rollup/plugin-terser": "^0.4.4", "d3-fetch": "^3.0.1", "d3-hierarchy": "^3.1.2", "d3-path": "^3.1.0", "d3-selection": "^3.0.0", "d3-transition": "^3.0.1", "d3-zoom": "^3.0.0", - "rollup": "^3.29.2" + "rollup": "^4.5.2" } } diff --git a/resources/css/pedigree-chart.css b/resources/css/pedigree-chart.css index 1320fd0..ae4bbe3 100644 --- a/resources/css/pedigree-chart.css +++ b/resources/css/pedigree-chart.css @@ -1,4 +1,11 @@ /* Form */ +.form-element-with-description { + padding-left: 1.5em; +} + +.form-element-with-description .form-check { + padding-left: 0; +} /* SVG */ .webtrees-pedigree-chart-container { diff --git a/resources/css/svg.css b/resources/css/svg.css index 0fcc778..9fa3b07 100644 --- a/resources/css/svg.css +++ b/resources/css/svg.css @@ -40,6 +40,7 @@ fill: none; stroke: rgb(175, 175, 175); stroke-width: 1.5px; + stroke-linecap: butt; /*shape-rendering: crispEdges;*/ } @@ -59,6 +60,8 @@ .webtrees-pedigree-chart-container svg .wt-chart-box-name-alt { fill: currentColor; + font-weight: 500; + font-size: 0.85em; } .webtrees-pedigree-chart-container svg .wt-chart-box-name:hover:not(.wt-chart-box-name-alt) { diff --git a/resources/js/modules/chart/elbow/horizontal.js b/resources/js/modules/chart/elbow/horizontal.js deleted file mode 100644 index 0dc03c1..0000000 --- a/resources/js/modules/chart/elbow/horizontal.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * This file is part of the package magicsunday/webtrees-pedigree-chart. - * - * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. - */ - -import * as d3 from "../../d3"; - -/** - * Draw the horizontal connecting lines between the profile boxes for Left/Right and Right/Left layout. - * - * @param {Link} link The link object - * @param {Orientation} orientation The current orientation - */ -export default function(link, orientation) -{ - const path = d3.path(); - - // Left => Right, Right => Left - const sourceX = link.source.x + (orientation.direction() * (orientation.boxWidth / 2)), - sourceY = link.source.y, - targetX = link.target.x - (orientation.direction() * (orientation.boxWidth / 2)), - targetY = link.target.y; - - path.moveTo(sourceX, sourceY); - path.lineTo((sourceX + ((targetX - sourceX) / 2)), sourceY); - path.lineTo((sourceX + ((targetX - sourceX) / 2)), targetY); - path.lineTo(targetX, targetY); - - return path.toString(); -} diff --git a/resources/js/modules/chart/elbow/vertical.js b/resources/js/modules/chart/elbow/vertical.js deleted file mode 100644 index dfdafe6..0000000 --- a/resources/js/modules/chart/elbow/vertical.js +++ /dev/null @@ -1,32 +0,0 @@ -/** - * This file is part of the package magicsunday/webtrees-pedigree-chart. - * - * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. - */ - -import * as d3 from "../../d3"; - -/** - * Draw the vertical connecting lines between the profile boxes for Top/Bottom and Bottom/Top layout. - * - * @param {Link} link The link object - * @param {Orientation} orientation The current orientation - */ -export default function(link, orientation) -{ - const path = d3.path(); - - // Top => Bottom, Bottom => Top - const sourceX = link.source.x, - sourceY = link.source.y + (orientation.direction() * (orientation.boxHeight / 2)), - targetX = link.target.x, - targetY = link.target.y - (orientation.direction() * (orientation.boxHeight / 2)); - - path.moveTo(sourceX, sourceY); - path.lineTo(sourceX, (sourceY + ((targetY - sourceY) / 2))); - path.lineTo(targetX, (sourceY + ((targetY - sourceY) / 2))); - path.lineTo(targetX, targetY); - - return path.toString(); -} diff --git a/resources/js/modules/configuration.js b/resources/js/modules/custom/configuration.js similarity index 95% rename from resources/js/modules/configuration.js rename to resources/js/modules/custom/configuration.js index 6adec66..341a9d2 100644 --- a/resources/js/modules/configuration.js +++ b/resources/js/modules/custom/configuration.js @@ -2,11 +2,11 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ -import {LAYOUT_LEFTRIGHT} from "./constants"; -import OrientationCollection from "./chart/orientation-collection"; +import OrientationCollection from "../lib/chart/orientation-collection"; +import {LAYOUT_LEFTRIGHT} from "../lib/constants"; /** * This class handles the configuration of the application. diff --git a/resources/js/modules/custom/data.js b/resources/js/modules/custom/data.js new file mode 100644 index 0000000..332f01e --- /dev/null +++ b/resources/js/modules/custom/data.js @@ -0,0 +1,84 @@ +/** + * This file is part of the package magicsunday/webtrees-pedigree-chart. + * + * For the full copyright and license information, please read the + * LICENSE file distributed with this source code. + */ + +import {Node} from "../lib/d3"; + +/** + * This files defines the internal used structures of objects. + * + * @author Rico Sonntag + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-pedigree-chart/ + */ + +/** + * The plain person data. + * + * @typedef {Object} Data + * @property {Number} id The unique ID of the person + * @property {String} xref The unique identifier of the person + * @property {String} sex The sex of the person + * @property {String} birth The birthdate of the person + * @property {String} death The death date of the person + * @property {String} timespan The lifetime description + * @property {String} thumbnail The URL of the thumbnail image + * @property {String} name The full name of the individual + * @property {String} preferredName The preferred first name + * @property {String[]} firstNames The list of first names + * @property {String[]} lastNames The list of last names + * @property {String} alternativeName The alternative name of the individual + */ + +/** + * A person object. + * + * @typedef {Object} Person + * @property {null|Data} data The data object of the individual + * @property {undefined|Object[]} parents The list of the parents of this individual + */ + +/** + * An individual. Extends the D3 Node object. + * + * @typedef {Node} Individual + * @property {Person} data The individual data + * @property {Individual[]} children The children of the node + * @property {Number} x The X-coordinate of the node + * @property {Number} y The Y-coordinate of the node + */ + +/** + * An X/Y coordinate. + * + * @typedef {Object} Coordinate + * @property {Number} x The X-coordinate + * @property {Number} y The Y-coordinate + */ + +/** + * A link between two nodes. + * + * @typedef {Object} Link + * @property {Individual} source The source individual + * @property {Individual} target The target individual + */ + +/** + * @typedef {Object} NameElementData + * @property {Data} data + * @property {Boolean} isRtl + * @property {Boolean} isAltRtl + * @property {Boolean} withImage + */ + +/** + * @typedef {Object} LabelElementData + * @property {String} label + * @property {Boolean} isPreferred + * @property {Boolean} isLastName + * @property {Boolean} isNameRtl + */ \ No newline at end of file diff --git a/resources/js/modules/chart/hierarchy.js b/resources/js/modules/custom/hierarchy.js similarity index 57% rename from resources/js/modules/chart/hierarchy.js rename to resources/js/modules/custom/hierarchy.js index 6f69940..aebec1c 100644 --- a/resources/js/modules/chart/hierarchy.js +++ b/resources/js/modules/custom/hierarchy.js @@ -2,11 +2,11 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ -import * as d3 from "../d3"; -import {SEX_FEMALE, SEX_MALE} from "../constants"; +import * as d3 from "../lib/d3"; +import {SEX_FEMALE, SEX_MALE} from "../lib/constants.js"; /** * This class handles the hierarchical data. @@ -41,7 +41,7 @@ export default class Hierarchy const maxGenerations = getDepth(data); // Construct root node from the hierarchical data - let root = d3.hierarchy( + this._root = d3.hierarchy( data, data => { if (!this._configuration.showEmptyBoxes) { @@ -49,11 +49,11 @@ export default class Hierarchy } // Fill up the missing parents to the requested number of generations - if (!data.parents && (data.generation < maxGenerations)) { - // if (!data.parents && (data.generation < this._configuration.generations)) { + if (!data.parents && (data.data.generation < maxGenerations)) { + // if (!data.parents && (data.data.generation < this._configuration.generations)) { data.parents = [ - this.createEmptyNode(data.generation + 1, SEX_MALE), - this.createEmptyNode(data.generation + 1, SEX_FEMALE) + this.createEmptyNode(data.data.generation + 1, SEX_MALE), + this.createEmptyNode(data.data.generation + 1, SEX_FEMALE) ]; } @@ -61,11 +61,11 @@ export default class Hierarchy if (data.parents && (data.parents.length < 2)) { if (data.parents[0].sex === SEX_MALE) { data.parents.push( - this.createEmptyNode(data.generation + 1, SEX_FEMALE) + this.createEmptyNode(data.data.generation + 1, SEX_FEMALE) ); } else { data.parents.unshift( - this.createEmptyNode(data.generation + 1, SEX_MALE) + this.createEmptyNode(data.data.generation + 1, SEX_MALE) ); } } @@ -73,20 +73,29 @@ export default class Hierarchy return data.parents; }); + // Assign a unique ID to each node + this._root.ancestors().forEach((d, i) => { + d.id = i; + }); + // Declares a tree layout and assigns the size - const treeLayout = d3.tree() - .nodeSize([this._configuration.orientation.nodeWidth, 0]) - .separation(() => 0.5); + const tree = d3.tree() + .nodeSize([this._configuration.orientation.nodeWidth, this._configuration.orientation.nodeHeight]) + .separation(() => 1.0); + + // Map the root node data to the tree layout + this._nodes = tree(this._root); - // Map the node data to the tree layout - this._root = root; - this._nodes = treeLayout(root); + // Normalize node coordinates (swap values for left/right layout) + this._root.each((node) => { + this._configuration.orientation.norm(node); + }); } /** * Returns the nodes. * - * @returns {Array} + * @returns {Individual[]} * * @public */ @@ -98,7 +107,7 @@ export default class Hierarchy /** * Returns the root note. * - * @returns {Object} + * @returns {Individual} * * @public */ @@ -113,26 +122,29 @@ export default class Hierarchy * @param {Number} generation Generation of the node * @param {String} sex The sex of the individual * - * @returns {Object} + * @returns {Data} * * @private */ createEmptyNode(generation, sex) { return { - id : 0, - xref : "", - url : "", - updateUrl : "", - generation : generation, - name : "", - firstNames : [], - lastNames : [], - preferredName : "", - alternativeNames : [], - isAltRtl : false, - sex : sex, - timespan : "" + data: { + id : 0, + xref : "", + url : "", + updateUrl : "", + generation : generation, + name : "", + isNameRtl : false, + firstNames : [], + lastNames : [], + preferredName : "", + alternativeName : "", + isAltRtl : false, + sex : "U", // sex + timespan : "" + } }; } } diff --git a/resources/js/modules/custom/tree.js b/resources/js/modules/custom/tree.js new file mode 100644 index 0000000..9340279 --- /dev/null +++ b/resources/js/modules/custom/tree.js @@ -0,0 +1,131 @@ +/** + * This file is part of the package magicsunday/webtrees-pedigree-chart. + * + * For the full copyright and license information, please read the + * LICENSE file distributed with this source code. + */ + +import * as d3 from "../lib/d3"; +import NodeDrawer from "../lib/tree/node-drawer"; +import LinkDrawer from "../lib/tree/link-drawer"; + +/** + * The class handles the creation of the tree. + * + * @author Rico Sonntag + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-pedigree-chart/ + */ +export default class Tree +{ + /** + * Constructor. + * + * @param {Svg} svg + * @param {Configuration} configuration The configuration + * @param {Hierarchy} hierarchy The hierarchical data + */ + constructor(svg, configuration, hierarchy) + { + this._svg = svg; + this._configuration = configuration; + this._hierarchy = hierarchy; + + this._hierarchy.root.x0 = 0; + this._hierarchy.root.y0 = 0; + + this._orientation = this._configuration.orientation; + + this._nodeDrawer = new NodeDrawer(this._svg, this._hierarchy, this._configuration); + this._linkDrawer = new LinkDrawer(this._svg, this._configuration); + + this.draw(this._hierarchy.root); + } + + /** + * Draw the tree. + * + * @param {Object} source The root object + * + * @public + */ + draw(source) + { + /** @type {Individual[]} */ + const nodes = this._hierarchy.root.descendants(); + + /** @type {Link[]} */ + const links = this._hierarchy.nodes.links(); + + // // Start with only the first few generations of ancestors showing + // nodes.forEach((person) => { + // if (person.parents) { + // person.parents.forEach((child) => this.collapse(child)); + // } + // }); + + // To avoid artifacts caused by rounding errors when drawing the links, + // we draw them first so that the nodes can then overlap them. + this._linkDrawer.drawLinks(links, source); + this._nodeDrawer.drawNodes(nodes, source); + } + + /** + * Centers the tree around all visible nodes. + */ + centerTree() + { + // TODO Doesn't work + + console.log("centerTree"); + // const zoom = this._svg.zoom.get(); + // + // d3.select(this._svg) + // // .transition() + // // .duration(0) + // // .delay(100) + // .call( + // zoom.transform, + // d3.zoomIdentity.translate(t.x, t.y).scale(t.k) + // ); + } + + /** + * Update a person's state when they are clicked. + */ + togglePerson(event, person) + { + if (person.parents) { + person._parents = person.parents; + person.parents = null; + } else { + person.parents = person._parents; + person._parents = null; + } + + this.draw(person); + } + + /** + * Collapse person (hide their ancestors). We recursively collapse the ancestors so that when the person is + * expanded it will only reveal one generation. If we don't recursively collapse the ancestors then when + * the person is clicked on again to expand, all ancestors that were previously showing will be shown again. + * If you want that behavior then just remove the recursion by removing the if block. + */ + collapse(person) + { + if (person.parents) { + person._parents = person.parents; + person._parents.forEach((parent) => this.collapse(parent)); + // person._parents.forEach(this.collapse); + person.parents = null; + } + + // person.collapsed = true; + // + // if (person.parents) { + // person.parents.forEach((child) => this.collapse(child)); + // person.parents.forEach(this.collapse); + // } + } +} diff --git a/resources/js/modules/data.js b/resources/js/modules/data.js deleted file mode 100644 index f36d4e6..0000000 --- a/resources/js/modules/data.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * This file is part of the package magicsunday/webtrees-descendants-chart. - * - * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. - */ - -import {Node} from "./d3"; - -/** - * This files defines the internal used structures of objects. - * - * @author Rico Sonntag - * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 - * @link https://github.com/magicsunday/webtrees-descendants-chart/ - */ - -/** - * The plain person data. - * - * @typedef {Object} Data - * @property {Number} id The unique ID of the person - * @property {String} xref The unique identifier of the person - * @property {String} sex The sex of the person - * @property {String} birth The birthdate of the person - * @property {String} death The death date of the person - * @property {String} timespan The lifetime description - * @property {String} thumbnail The URL of the thumbnail image - * @property {String} name The full name of the individual - * @property {String} preferredName The preferred first name - * @property {String[]} firstNames The list of first names - * @property {String[]} lastNames The list of last names - */ - -/** - * A person object. - * - * @typedef {Object} Person - * @property {null|Data} data The data object of the individual - */ - -/** - * An individual. Extends the D3 Node object. - * - * @typedef {Node} Individual - * @property {Person} data The individual data - * @property {Individual[]} parents The parents of the node - * @property {Number} x The X-coordinate of the node - * @property {Number} y The Y-coordinate of the node - */ - -/** - * An X/Y coordinate. - * - * @typedef {Object} Coordinate - * @property {Number} x The X-coordinate - * @property {Number} y The Y-coordinate - */ - -/** - * A link between two nodes. - * - * @typedef {Object} Link - * @property {Individual} source The source individual - * @property {Individual} target The target individual - */ diff --git a/resources/js/modules/index.js b/resources/js/modules/index.js index 21471e2..4b10543 100644 --- a/resources/js/modules/index.js +++ b/resources/js/modules/index.js @@ -2,12 +2,12 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ -import * as d3 from "./d3"; -import Configuration from "./configuration"; -import Chart from "./chart"; +import * as d3 from "./lib/d3"; +import Configuration from "./custom/configuration"; +import Chart from "./lib/chart"; /** * The application class. @@ -140,6 +140,11 @@ export class PedigreeChart { this._chart.svg .export('svg') - .svgToImage(this._chart.svg, this._cssFiles, "pedigree-chart.svg"); + .svgToImage( + this._chart.svg, + this._cssFiles, + "webtrees-pedigree-chart-container", + "pedigree-chart.svg" + ); } } diff --git a/resources/js/modules/chart.js b/resources/js/modules/lib/chart.js similarity index 66% rename from resources/js/modules/chart.js rename to resources/js/modules/lib/chart.js index fe1e1f4..c10c405 100644 --- a/resources/js/modules/chart.js +++ b/resources/js/modules/lib/chart.js @@ -2,13 +2,13 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import * as d3 from "./d3"; -import Hierarchy from "./chart/hierarchy"; -import Tree from "./tree"; -import Overlay from "./chart/overlay"; +import Hierarchy from "../custom/hierarchy"; import Svg from "./chart/svg"; +import Overlay from "./chart/overlay"; +import Tree from "../custom/tree"; const MIN_HEIGHT = 300; const MIN_PADDING = 10; // Minimum padding around view box @@ -93,7 +93,7 @@ export default class Chart /** * Returns the chart data. * - * @returns {Object} + * @returns {Data} */ get data() { @@ -103,7 +103,7 @@ export default class Chart /** * Sets the chart data. * - * @param {Object} value The chart data + * @param {Data} value The chart data */ set data(value) { @@ -130,35 +130,21 @@ export default class Chart // Init the events this._svg.initEvents(this._overlay); - let tree = new Tree(this._svg, this._configuration, this._hierarchy); - - // let personGroup = this._svg.get().select("g.personGroup"); - // let gradient = new Gradient(this._svg, this._configuration); - // let that = this; - // - // personGroup - // .selectAll("g.person") - // .data(this._hierarchy.nodes, (d) => d.data.id) - // .enter() - // .append("g") - // .attr("class", "person") - // .attr("id", (d) => "person-" + d.data.id); - // - // // Create a new selection in order to leave the previous enter() selection - // personGroup - // .selectAll("g.person") - // .each(function (d) { - // let person = d3.select(this); - // - // if (that._configuration.showColorGradients) { - // gradient.init(d); - // } - // - // new Person(that._svg, that._configuration, person, d); - // }); + // Create tree + new Tree(this._svg, this._configuration, this._hierarchy); + // TODO Add separate button to toggle transition to keep clicking? this.bindClickEventListener(); + this.updateViewBox(); + +// const width = 1296; +// const height = (this._hierarchy.root.descendants().length + 1) * this._configuration.orientation.nodeWidth; +// +// console.log('viewBox', [-this._configuration.orientation.nodeWidth / 2, -this._configuration.orientation.nodeWidth * 3 / 2, width, height]); +// +// this._svg.get() +// .attr("viewBox", [-this._configuration.orientation.nodeWidth / 2, -this._configuration.orientation.nodeWidth * 3 / 2, width, height]) } /** @@ -170,14 +156,14 @@ export default class Chart this._svg.visual .selectAll("g.person") - .filter((d) => d.data.xref !== "") - .each(function (d) { - d3.select(this).on("click", function() { that.personClick(d.data); }); + .filter(person => person.data.data.xref !== "") + .each(function (person) { + d3.select(this).on("click", function() { that.personClick(person.data); }); }); } /** - * Method triggers either the "update" or "individual" method on the click on an person. + * Method triggers either the "update" or "individual" method on the click on a person. * * @param {Object} data The D3 data object * @@ -186,7 +172,7 @@ export default class Chart personClick(data) { // Trigger either "update" or "redirectToIndividual" method on click depending on person in chart - (data.generation === 1) ? this.redirectToIndividual(data.url) : this.update(data.updateUrl); + (data.data.generation === 1) ? this.redirectToIndividual(data.data.url) : this.update(data.data.updateUrl); } /** @@ -208,34 +194,7 @@ export default class Chart */ update(url) { + // See update.js for a possible AJAX only update solution, but which requires some additional work window.location = url; } - - // /** - // * Changes root individual - // * - // * @param {String} url The update url - // * - // * @private - // */ - // update(url) - // { - // var that = this; - // - // $.getJSON(url, function(data) { - // that.data = data; - // that.draw(); - // - // var indSelector = $(document.getElementById('xref')); - // $.ajax({ - // type: 'POST', - // url: indSelector.attr("data-ajax--url"), - // data: { q : data.xref } - // }).then(function (data) { - // // create the option and append to Select2 - // var option = new Option(data.results[0].text, data.results[0].id, true, true); - // indSelector.append(option).trigger('change'); - // }); - // }); - // } } diff --git a/resources/js/modules/chart/box.js b/resources/js/modules/lib/chart/box.js similarity index 82% rename from resources/js/modules/chart/box.js rename to resources/js/modules/lib/chart/box.js index 9bd845f..d60d33d 100644 --- a/resources/js/modules/chart/box.js +++ b/resources/js/modules/lib/chart/box.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import Image from "./box/image"; @@ -26,7 +26,6 @@ export default class Box { // The default corner radius this._cornerRadius = 20; - this._showImage = true; this._orientation = orientation; // Calculate values @@ -40,26 +39,6 @@ export default class Box this._image = new Image(orientation, this._cornerRadius); } - /** - * Returns TRUE if image should be displayed otherwise FALSE. - * - * @returns {Boolean} - */ - get showImage() - { - return this._showImage; - } - - /** - * Set TRUE to show image or FALSE to hide. - * - * @param {Boolean} value - */ - set showImage(value) - { - this._showImage = value; - } - /** * Returns the X-coordinate of the center of the box. * @@ -139,7 +118,7 @@ export default class Box { return new Text( this._orientation, - this._showImage ? this._image : null + this._image ); } } diff --git a/resources/js/modules/chart/box/image.js b/resources/js/modules/lib/chart/box/image.js similarity index 78% rename from resources/js/modules/chart/box/image.js rename to resources/js/modules/lib/chart/box/image.js index d3da4bb..5d570a6 100644 --- a/resources/js/modules/chart/box/image.js +++ b/resources/js/modules/lib/chart/box/image.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import OrientationLeftRight from "../orientation/orientation-leftRight"; @@ -27,16 +27,17 @@ export default class Image { this._orientation = orientation; this._cornerRadius = cornerRadius; + this._imagePadding = 5; - this._imageRadius = Math.min(40, (orientation.boxHeight / 2) - this._imagePadding); + this._imageRadius = Math.min(40, (this._orientation.boxHeight / 2) - this._imagePadding); // Calculate values - this._x = this.calculateX(); - this._y = this.calculateY(); this._width = this.calculateImageWidth(); this._height = this.calculateImageHeight(); this._rx = this.calculateCornerRadius(); this._ry = this.calculateCornerRadius(); + this._x = this.calculateX(); + this._y = this.calculateY(); } /** @@ -46,7 +47,15 @@ export default class Image */ calculateX() { - return -(this._orientation.boxWidth / 2) + this._imagePadding; + if ((this._orientation instanceof OrientationLeftRight) + || (this._orientation instanceof OrientationRightLeft) + ) { + return this._orientation.isDocumentRtl + ? (this._width - this._imagePadding) + : (-(this._orientation.boxWidth - this._imagePadding) / 2) + this._imagePadding; + } + + return -(this._orientation.boxWidth / 2) + (this._width / 2); } /** @@ -62,7 +71,7 @@ export default class Image return -this._imageRadius; } - return -(this._orientation.boxHeight / 2) + this._imagePadding; + return -((this._orientation.boxHeight - this._imagePadding) / 2) + this._imagePadding; } /** @@ -72,13 +81,7 @@ export default class Image */ calculateImageWidth() { - if ((this._orientation instanceof OrientationLeftRight) - || (this._orientation instanceof OrientationRightLeft) - ) { - return this._imageRadius * 2; - } - - return this._orientation.boxWidth - (this._imagePadding * 2); + return this._imageRadius * 2; } /** @@ -101,36 +104,6 @@ export default class Image return this._cornerRadius - this._imagePadding; } - /** - * Returns the amount of image padding. - * - * @returns {Number} - */ - get imagePadding() - { - return this._imagePadding; - } - - /** - * Returns the radius of the image. - * - * @returns {Number} - */ - get imageRadius() - { - return this._imageRadius; - } - - /** - * Sets the radius of the image. - * - * @param {Number} value The new radius - */ - set imageRadius(value) - { - this._imageRadius = value; - } - /** * Returns the X-coordinate of the center of the image. * diff --git a/resources/js/modules/chart/box/text.js b/resources/js/modules/lib/chart/box/text.js similarity index 66% rename from resources/js/modules/chart/box/text.js rename to resources/js/modules/lib/chart/box/text.js index b7a4dd3..24f464c 100644 --- a/resources/js/modules/chart/box/text.js +++ b/resources/js/modules/lib/chart/box/text.js @@ -2,11 +2,13 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import OrientationLeftRight from "../orientation/orientation-leftRight"; import OrientationRightLeft from "../orientation/orientation-rightLeft"; +import OrientationTopBottom from "../orientation/orientation-topBottom"; +import OrientationBottomTop from "../orientation/orientation-bottomTop"; /** * The person text box container. @@ -25,9 +27,17 @@ export default class Text */ constructor(orientation, image = null) { - this._orientation = orientation; - this._image = image; - this._textPadding = 15; + this._orientation = orientation; + this._image = image; + this._textPaddingX = 15; + this._textPaddingY = 15; + + if ((this._orientation instanceof OrientationTopBottom) + || (this._orientation instanceof OrientationBottomTop) + ) { + this._textPaddingX = 5; + this._textPaddingY = 15; + } // Calculate values this._x = this.calculateX(); @@ -42,14 +52,7 @@ export default class Text */ calculateX() { - const xStart = -(this._orientation.boxWidth / 2) + this._textPadding; - - if (!this._image) { - return xStart; - } - - // Adjust x position by width of image - return xStart + this._image.width; + return -(this._orientation.boxWidth / 2) + this._textPaddingX; } /** @@ -62,14 +65,10 @@ export default class Text if ((this._orientation instanceof OrientationLeftRight) || (this._orientation instanceof OrientationRightLeft) ) { - return -this._textPadding; - } - - if (!this._image) { - return -(this._orientation.boxHeight / 2) + (this._textPadding * 2); + return -this._textPaddingY; } - return this._image.y + this._image.height + (this._textPadding * 2); + return this._image.y + this._image.height + (this._textPaddingY * 2); } /** @@ -79,20 +78,8 @@ export default class Text */ calculateWidth() { - // Width of box minus the right/left padding - const defaultWidth = this._orientation.boxWidth - (this._textPadding * 2); - - if (!this._image) { - return defaultWidth; - } - - if ((this._orientation instanceof OrientationLeftRight) - || (this._orientation instanceof OrientationRightLeft) - ) { - return defaultWidth - this._image.width; - } - - return defaultWidth; + // Width of the text minus the right/left padding + return this._orientation.boxWidth - (this._textPaddingX * 2); } /** @@ -123,5 +110,6 @@ export default class Text get width() { return this._width; + } } diff --git a/resources/js/modules/chart/orientation-collection.js b/resources/js/modules/lib/chart/orientation-collection.js similarity index 96% rename from resources/js/modules/chart/orientation-collection.js rename to resources/js/modules/lib/chart/orientation-collection.js index 0db015b..dd00572 100644 --- a/resources/js/modules/chart/orientation-collection.js +++ b/resources/js/modules/lib/chart/orientation-collection.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import { diff --git a/resources/js/modules/chart/orientation/orientation-bottomTop.js b/resources/js/modules/lib/chart/orientation/orientation-bottomTop.js similarity index 72% rename from resources/js/modules/chart/orientation/orientation-bottomTop.js rename to resources/js/modules/lib/chart/orientation/orientation-bottomTop.js index 1501e95..2452c87 100644 --- a/resources/js/modules/chart/orientation/orientation-bottomTop.js +++ b/resources/js/modules/lib/chart/orientation/orientation-bottomTop.js @@ -2,11 +2,11 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import Orientation from "./orientation"; -import elbowVertical from "../elbow/vertical"; +import elbowVertical from "../../tree/elbow/vertical"; /** * This class handles the orientation of the tree. @@ -30,23 +30,28 @@ export default class OrientationBottomTop extends Orientation this._splittNames = true; } - direction() + get direction() { return -1; } get nodeWidth() { - return (this._boxWidth * 2) + 30; + return this._boxWidth + this._xOffset; + } + + get nodeHeight() + { + return this._boxHeight + this._yOffset; } norm(d) { - d.y = this.direction() * d.depth * (this._boxHeight + 30); + d.y *= this.direction; } - elbow(d) + elbow(link) { - return elbowVertical(d, this); + return elbowVertical(link, this); } } diff --git a/resources/js/modules/chart/orientation/orientation-leftRight.js b/resources/js/modules/lib/chart/orientation/orientation-leftRight.js similarity index 65% rename from resources/js/modules/chart/orientation/orientation-leftRight.js rename to resources/js/modules/lib/chart/orientation/orientation-leftRight.js index c09ceb1..496ff1c 100644 --- a/resources/js/modules/chart/orientation/orientation-leftRight.js +++ b/resources/js/modules/lib/chart/orientation/orientation-leftRight.js @@ -2,11 +2,11 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import Orientation from "./orientation"; -import elbowHorizontal from "../elbow/horizontal"; +import elbowHorizontal from "../../tree/elbow/horizontal"; /** * This class handles the orientation of the tree. @@ -26,29 +26,34 @@ export default class OrientationLeftRight extends Orientation constructor(boxWidth, boxHeight) { super(boxWidth, boxHeight); + + this._xOffset = 40; + this._yOffset = 20; } - direction() + get direction() { - return 1; + return this.isDocumentRtl ? -1 : 1; } get nodeWidth() { - return (this._boxHeight * 2) + 30; + return this._boxHeight + this._yOffset; } - norm(d) + get nodeHeight() { - const oldX = d.x; + return this._boxWidth + this._xOffset; + } + norm(d) + { // Swap x and y values - d.x = this.direction() * d.depth * (this._boxWidth + 30); - d.y = oldX; + [d.x, d.y] = [d.y * this.direction, d.x]; } - elbow(d) + elbow(link) { - return elbowHorizontal(d, this); + return elbowHorizontal(link, this); } } diff --git a/resources/js/modules/chart/orientation/orientation-rightLeft.js b/resources/js/modules/lib/chart/orientation/orientation-rightLeft.js similarity index 65% rename from resources/js/modules/chart/orientation/orientation-rightLeft.js rename to resources/js/modules/lib/chart/orientation/orientation-rightLeft.js index 65bc573..0b20692 100644 --- a/resources/js/modules/chart/orientation/orientation-rightLeft.js +++ b/resources/js/modules/lib/chart/orientation/orientation-rightLeft.js @@ -2,11 +2,11 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import Orientation from "./orientation"; -import elbowHorizontal from "../elbow/horizontal"; +import elbowHorizontal from "../../tree/elbow/horizontal"; /** * This class handles the orientation of the tree. @@ -26,29 +26,34 @@ export default class OrientationRightLeft extends Orientation constructor(boxWidth, boxHeight) { super(boxWidth, boxHeight); + + this._xOffset = 40; + this._yOffset = 20; } - direction() + get direction() { - return -1; + return this.isDocumentRtl ? 1 : -1; } get nodeWidth() { - return (this._boxHeight * 2) + 30; + return this._boxHeight + this._yOffset; } - norm(d) + get nodeHeight() { - const oldX = d.x; + return this._boxWidth + this._xOffset; + } + norm(d) + { // Swap x and y values - d.x = this.direction() * d.depth * (this._boxWidth + 30); - d.y = oldX; + [d.x, d.y] = [d.y * this.direction, d.x]; } - elbow(d) + elbow(link) { - return elbowHorizontal(d, this); + return elbowHorizontal(link, this); } } diff --git a/resources/js/modules/chart/orientation/orientation-topBottom.js b/resources/js/modules/lib/chart/orientation/orientation-topBottom.js similarity index 72% rename from resources/js/modules/chart/orientation/orientation-topBottom.js rename to resources/js/modules/lib/chart/orientation/orientation-topBottom.js index 8f2b4d8..0b62aa4 100644 --- a/resources/js/modules/chart/orientation/orientation-topBottom.js +++ b/resources/js/modules/lib/chart/orientation/orientation-topBottom.js @@ -2,11 +2,11 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import Orientation from "./orientation"; -import elbowVertical from "../elbow/vertical"; +import elbowVertical from "../../tree/elbow/vertical"; /** * This class handles the orientation of the tree. @@ -30,23 +30,28 @@ export default class OrientationTopBottom extends Orientation this._splittNames = true; } - direction() + get direction() { return 1; } get nodeWidth() { - return (this._boxWidth * 2) + 30; + return this._boxWidth + this._xOffset; + } + + get nodeHeight() + { + return this._boxHeight + this._yOffset; } norm(d) { - d.y = this.direction() * d.depth * (this._boxHeight + 30); + d.y *= this.direction; } - elbow(d) + elbow(link) { - return elbowVertical(d, this); + return elbowVertical(link, this); } } diff --git a/resources/js/modules/chart/orientation/orientation.js b/resources/js/modules/lib/chart/orientation/orientation.js similarity index 66% rename from resources/js/modules/chart/orientation/orientation.js rename to resources/js/modules/lib/chart/orientation/orientation.js index b830299..b6f4135 100644 --- a/resources/js/modules/chart/orientation/orientation.js +++ b/resources/js/modules/lib/chart/orientation/orientation.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ /** @@ -22,11 +22,45 @@ export default class Orientation */ constructor(boxWidth, boxHeight) { + // The distance between single nodes + this._xOffset = 30; + this._yOffset = 60; + this._boxWidth = boxWidth; this._boxHeight = boxHeight; this._splittNames = false; } + /** + * Returns TRUE if the document is in RTL direction. + * + * @return {Boolean} + */ + get isDocumentRtl() + { + return document.dir === "rtl"; + } + + /** + * Returns the x-offset between two boxes. + * + * @returns {Number} + */ + get xOffset() + { + return this._xOffset; + } + + /** + * Returns the y-offset between two boxes. + * + * @returns {Number} + */ + get yOffset() + { + return this._yOffset; + } + /** * Returns whether to splitt the names on multiple lines or not. * @@ -62,7 +96,7 @@ export default class Orientation * * @returns {Number} */ - direction() + get direction() { throw "Abstract method direction() not implemented"; } @@ -72,13 +106,25 @@ export default class Orientation * * @returns {Number} */ - nodeWidth() + get nodeWidth() { throw "Abstract method nodeWidth() not implemented"; } + /** + * Returns the height of the node. + * + * @returns {Number} + */ + get nodeHeight() + { + throw "Abstract method nodeHeight() not implemented"; + } + /** * Normalizes the x and/or y values of an entry. + * + * @param {Individual} d */ norm(d) { @@ -88,9 +134,11 @@ export default class Orientation /** * Returns the elbow function depending on the orientation. * + * @param {Link} link + * * @returns {String} */ - elbow(d) + elbow(link) { throw "Abstract method elbow() not implemented"; } diff --git a/resources/js/modules/chart/overlay.js b/resources/js/modules/lib/chart/overlay.js similarity index 97% rename from resources/js/modules/chart/overlay.js rename to resources/js/modules/lib/chart/overlay.js index 8ba2dab..1928327 100644 --- a/resources/js/modules/chart/overlay.js +++ b/resources/js/modules/lib/chart/overlay.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ /** diff --git a/resources/js/modules/chart/svg.js b/resources/js/modules/lib/chart/svg.js similarity index 93% rename from resources/js/modules/chart/svg.js rename to resources/js/modules/lib/chart/svg.js index 6dfeb65..2d9ec62 100644 --- a/resources/js/modules/chart/svg.js +++ b/resources/js/modules/lib/chart/svg.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import Defs from "./svg/defs"; @@ -56,7 +56,7 @@ export default class Svg } /** - * Initialiaze the element events. + * Initialize the element events. * * @param {Overlay} overlay */ @@ -70,22 +70,22 @@ export default class Svg this._configuration.labels.zoom, 300, () => { - overlay.hide(700, 800); + overlay.hide(200, 600); } ); } }) .on("touchend", (event) => { if (event.touches.length < 2) { - overlay.hide(0, 800); + overlay.hide(0, 600); } }) .on("touchmove", (event) => { if (event.touches.length >= 2) { - // Hide tooltip on more than 2 fingers + // Hide tooltip on more than two fingers overlay.hide(); } else { - // Show tooltip if less than 2 fingers are used + // Show tooltip if less than two fingers are used overlay.show(this._configuration.labels.move); } }) diff --git a/resources/js/modules/chart/svg/defs.js b/resources/js/modules/lib/chart/svg/defs.js similarity index 92% rename from resources/js/modules/chart/svg/defs.js rename to resources/js/modules/lib/chart/svg/defs.js index cc8f98a..7ad340f 100644 --- a/resources/js/modules/chart/svg/defs.js +++ b/resources/js/modules/lib/chart/svg/defs.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ /** diff --git a/resources/js/modules/chart/svg/export-factory.js b/resources/js/modules/lib/chart/svg/export-factory.js similarity index 94% rename from resources/js/modules/chart/svg/export-factory.js rename to resources/js/modules/lib/chart/svg/export-factory.js index 02a2757..133295a 100644 --- a/resources/js/modules/chart/svg/export-factory.js +++ b/resources/js/modules/lib/chart/svg/export-factory.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import PngExport from "./export/png"; diff --git a/resources/js/modules/chart/svg/export.js b/resources/js/modules/lib/chart/svg/export.js similarity index 91% rename from resources/js/modules/chart/svg/export.js rename to resources/js/modules/lib/chart/svg/export.js index c6ca65f..f904d1f 100644 --- a/resources/js/modules/chart/svg/export.js +++ b/resources/js/modules/lib/chart/svg/export.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ /** @@ -15,7 +15,7 @@ export default class Export { /** - * Triggers the download by creating a new anchor element an simulate a mouse click on it. + * Triggers the download by creating a new anchor element and simulate a mouse click on it. * * @param {String} imgURI The image URI data stream * @param {String} fileName The file name to use in the download dialog diff --git a/resources/js/modules/chart/svg/export/png.js b/resources/js/modules/lib/chart/svg/export/png.js similarity index 97% rename from resources/js/modules/chart/svg/export/png.js rename to resources/js/modules/lib/chart/svg/export/png.js index 1f68aba..a164560 100644 --- a/resources/js/modules/chart/svg/export/png.js +++ b/resources/js/modules/lib/chart/svg/export/png.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import Export from "../export"; @@ -44,7 +44,7 @@ export default class PngExport extends Export } /** - * Returns the viewbox of the SVG. Mainly used to apply a padding around the chart. + * Returns the view-box of the SVG. Mainly used to apply a padding around the chart. * * @param {SVGGraphicsElement} svg The SVG element * diff --git a/resources/js/modules/chart/svg/export/svg.js b/resources/js/modules/lib/chart/svg/export/svg.js similarity index 78% rename from resources/js/modules/chart/svg/export/svg.js rename to resources/js/modules/lib/chart/svg/export/svg.js index 82696d1..f6cd1da 100644 --- a/resources/js/modules/chart/svg/export/svg.js +++ b/resources/js/modules/lib/chart/svg/export/svg.js @@ -1,8 +1,8 @@ /** - * This file is part of the package magicsunday/webtrees-pedigree-chart. + * This file is part of the package magicsunday/webtrees-descendants-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import * as d3 from "../../../d3"; @@ -13,7 +13,7 @@ import Export from "../export"; * * @author Rico Sonntag * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 - * @link https://github.com/magicsunday/webtrees-pedigree-chart/ + * @link https://github.com/magicsunday/webtrees-descendants-chart/ */ export default class SvgExport extends Export { @@ -23,10 +23,11 @@ export default class SvgExport extends Export * * @param {String[]} cssFiles * @param {SVGGraphicsElement} destinationNode + * @param {String} containerClassName The container class name * * @returns {Promise} */ - copyStylesInline(cssFiles, destinationNode) + copyStylesInline(cssFiles, destinationNode, containerClassName) { return new Promise(resolve => { Promise @@ -34,7 +35,7 @@ export default class SvgExport extends Export .then((filesData) => { filesData.forEach(data => { // Remove parent container selector as the CSS is included directly in the SVG element - data = data.replace(/.webtrees-pedigree-chart-container /g, ""); + data = data.replace(new RegExp("." + containerClassName + " ", "g"), ""); let style = document.createElementNS("http://www.w3.org/2000/svg", "style"); style.appendChild(document.createTextNode(data)); @@ -93,14 +94,15 @@ export default class SvgExport extends Export /** * Saves the given SVG as SVG image file. * - * @param {Svg} svg The source SVG object - * @param {String[]} cssFiles The CSS files used together with the SVG - * @param {String} fileName The output file name + * @param {Svg} svg The source SVG object + * @param {String[]} cssFiles The CSS files used together with the SVG + * @param {String} containerClassName The container class name + * @param {String} fileName The output file name */ - svgToImage(svg, cssFiles, fileName) + svgToImage(svg, cssFiles, containerClassName, fileName) { this.cloneSvg(svg.get().node()) - .then(newSvg => this.copyStylesInline(cssFiles, newSvg)) + .then(newSvg => this.copyStylesInline(cssFiles, newSvg, containerClassName)) .then(newSvg => this.convertToObjectUrl(newSvg)) .then(objectUrl => this.triggerDownload(objectUrl, fileName)) .catch(() => { diff --git a/resources/js/modules/chart/svg/zoom.js b/resources/js/modules/lib/chart/svg/zoom.js similarity index 84% rename from resources/js/modules/chart/svg/zoom.js rename to resources/js/modules/lib/chart/svg/zoom.js index 79cee71..eacc423 100644 --- a/resources/js/modules/chart/svg/zoom.js +++ b/resources/js/modules/lib/chart/svg/zoom.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import * as d3 from "./../../d3"; @@ -61,25 +61,25 @@ export default class Zoom // Add zoom filter this._zoom.filter((event) => { - // Allow "wheel" event only while control key is pressed + // Allow "wheel" event only while the control key is pressed if (event.type === "wheel") { if (!event.ctrlKey) { return false; } - var transform = d3.zoomTransform(this); + const transform = d3.zoomTransform(this); if (transform.k) { - // Prevent zooming below lowest level + // Prevent zooming below the lowest level if ((transform.k <= MIN_ZOOM) && (event.deltaY > 0)) { - // Prevent browsers page zoom while holding down the control key + // Prevent browser page zoom while holding down the control key event.preventDefault(); return false; } // Prevent zooming above highest level if ((transform.k >= MAX_ZOOM) && (event.deltaY < 0)) { - // Prevent browsers page zoom while holding down the control key + // Prevent browser page zoom while holding down the control key event.preventDefault(); return false; } @@ -98,7 +98,7 @@ export default class Zoom } /** - * Returns the internal d3 zoom behaviour. + * Returns the internal d3 zoom behavior. * * @returns {zoom} */ diff --git a/resources/js/modules/lib/chart/text/measure.js b/resources/js/modules/lib/chart/text/measure.js new file mode 100644 index 0000000..3116caa --- /dev/null +++ b/resources/js/modules/lib/chart/text/measure.js @@ -0,0 +1,34 @@ +/** + * This file is part of the package magicsunday/webtrees-pedigree-chart. + * + * For the full copyright and license information, please read the + * LICENSE file distributed with this source code. + */ + +let measureCanvas = null; + +/** + * Measures the given text and return its width depending on the used font (including size and weight). + * + * @param {String} text The text whose length is to be determined + * @param {String} fontFamily The font family used to calculate the length + * @param {String} fontSize The font size used to calculate the length + * @param {Number} fontWeight The font weight used to calculate the length + * + * @returns {Number} + */ +export default function(text, fontFamily, fontSize, fontWeight = 400) +{ + if (measureCanvas === null) { + measureCanvas = document.createElement("canvas"); + } + + const context = measureCanvas.getContext("2d"); + const font = `${fontWeight || ''} ${fontSize} ${fontFamily}`; + + if (context.font !== font) { + context.font = font; + } + + return context.measureText(text).width; +} diff --git a/resources/js/modules/chart/update.js b/resources/js/modules/lib/chart/update.js similarity index 95% rename from resources/js/modules/chart/update.js rename to resources/js/modules/lib/chart/update.js index e476bac..f7edc35 100644 --- a/resources/js/modules/chart/update.js +++ b/resources/js/modules/lib/chart/update.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ import * as d3 from "./../d3"; @@ -62,7 +62,7 @@ export default class Update type: "POST", url: indSelector.attr("data-ajax--url"), data: { - q : data.xref + q : data.data.xref } }).then(function (data) { // Create the option and append to Select2 diff --git a/resources/js/modules/common/dataUrl.js b/resources/js/modules/lib/common/dataUrl.js similarity index 57% rename from resources/js/modules/common/dataUrl.js rename to resources/js/modules/lib/common/dataUrl.js index 0f0734b..e061197 100644 --- a/resources/js/modules/common/dataUrl.js +++ b/resources/js/modules/lib/common/dataUrl.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ /** @@ -18,12 +18,12 @@ export default function(input, init = null) return fetch(input, init) .then(response => response.blob()) .then(blob => new Promise( - (resolve, reject) => { - const reader = new FileReader(); - reader.onloadend = () => resolve(reader.result); - reader.onerror = reject; - reader.readAsDataURL(blob); - } - ) - ); + (resolve, reject) => { + const reader = new FileReader(); + reader.onloadend = () => resolve(reader.result); + reader.onerror = reject; + reader.readAsDataURL(blob); + } + ) + ); } diff --git a/resources/js/modules/common/dpi.js b/resources/js/modules/lib/common/dpi.js similarity index 89% rename from resources/js/modules/common/dpi.js rename to resources/js/modules/lib/common/dpi.js index 1f9728c..c957262 100644 --- a/resources/js/modules/common/dpi.js +++ b/resources/js/modules/lib/common/dpi.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ /** diff --git a/resources/js/modules/constants.js b/resources/js/modules/lib/constants.js similarity index 75% rename from resources/js/modules/constants.js rename to resources/js/modules/lib/constants.js index eab2c84..613db40 100644 --- a/resources/js/modules/constants.js +++ b/resources/js/modules/lib/constants.js @@ -2,23 +2,25 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ /** * The widths and heights of a single node in each tree layout. * * @type {Number} + * @const */ export const LAYOUT_HORIZONTAL_NODE_WIDTH = 325; -export const LAYOUT_HORIZONTAL_NODE_HEIGHT = 80; -export const LAYOUT_VERTICAL_NODE_WIDTH = 150; -export const LAYOUT_VERTICAL_NODE_HEIGHT = 175; +export const LAYOUT_HORIZONTAL_NODE_HEIGHT = 95; +export const LAYOUT_VERTICAL_NODE_WIDTH = 160; +export const LAYOUT_VERTICAL_NODE_HEIGHT = 205; /** * Tree layout variants. * * @type {String} + * @const * * @see \Fisharebest\Webtrees\Module\PedigreeChartModule */ @@ -31,6 +33,7 @@ export const LAYOUT_RIGHTLEFT = "left"; * Gender types. * * @type {String} + * @const */ export const SEX_MALE = "M"; export const SEX_FEMALE = "F"; diff --git a/resources/js/modules/d3.js b/resources/js/modules/lib/d3.js similarity index 86% rename from resources/js/modules/d3.js rename to resources/js/modules/lib/d3.js index 9137787..0953305 100644 --- a/resources/js/modules/d3.js +++ b/resources/js/modules/lib/d3.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ /* @@ -19,7 +19,7 @@ export { } from "d3-fetch"; export { - Node, hierarchy, partition, tree + Node, hierarchy, tree } from "d3-hierarchy"; export { diff --git a/resources/js/modules/storage.js b/resources/js/modules/lib/storage.js similarity index 84% rename from resources/js/modules/storage.js rename to resources/js/modules/lib/storage.js index 927bfa5..007cd52 100644 --- a/resources/js/modules/storage.js +++ b/resources/js/modules/lib/storage.js @@ -2,7 +2,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ /** @@ -26,9 +26,9 @@ export class Storage } /** - * Register a HTML element. + * Register an HTML element. * - * @param {String} name The id or name of a HTML element + * @param {String} name The id or name of an HTML element */ register(name) { @@ -52,7 +52,7 @@ export class Storage } /** - * This methods stores the value of an input element depending on its type. + * This method stores the value of an input element depending on its type. * * @param {EventTarget|HTMLInputElement} element The HTML input element */ @@ -68,7 +68,7 @@ export class Storage /** * Returns the stored value belonging to the HTML element id. * - * @param {String} name The id or name of a HTML element + * @param {String} name The id or name of an HTML element * * @returns {String|Boolean|Number} */ @@ -80,7 +80,7 @@ export class Storage /** * Stores a value to the given HTML element id. * - * @param {String} name The id or name of a HTML element + * @param {String} name The id or name of an HTML element * @param {String|Boolean|Number} value The value to store */ write(name, value) diff --git a/resources/js/modules/lib/tree/date.js b/resources/js/modules/lib/tree/date.js new file mode 100644 index 0000000..ba8a158 --- /dev/null +++ b/resources/js/modules/lib/tree/date.js @@ -0,0 +1,203 @@ +/** + * This file is part of the package magicsunday/webtrees-pedigree-chart. + * + * For the full copyright and license information, please read the + * LICENSE file distributed with this source code. + */ + +import measureText from "../chart/text/measure" +import OrientationTopBottom from "../chart/orientation/orientation-topBottom"; +import OrientationBottomTop from "../chart/orientation/orientation-bottomTop"; + +/** + * The class handles the creation of the tree. + * + * @author Rico Sonntag + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-pedigree-chart/ + */ +export default class Date +{ + /** + * Constructor. + * + * @param {Svg} svg + * @param {Orientation} orientation + * @param {Image} image + * @param {Text} text + */ + constructor(svg, orientation, image, text) + { + this._svg = svg; + this._orientation = orientation; + this._image = image; + this._text = text; + } + + /** + * Add the individual dates to the given parent element. + * + * @param {selection} parent The parent element to which the elements are to be attached + * + * @public + */ + appendDate(parent) + { + const table = parent + .append("g") + .attr("class", "table"); + + // Top/Bottom and Bottom/Top + if ((this._orientation instanceof OrientationTopBottom) + || (this._orientation instanceof OrientationBottomTop) + ) { + const enter = table.selectAll("text.date") + .data(d => [{ + label: d.data.data.timespan, + withImage: true + }]) + .enter() + + const text = enter.append("text") + .attr("class", "date") + .attr("text-anchor", "middle") + .attr("alignment-baseline", "central") + .attr("y", this._text.y + 75); + + text.append("title") + .text(d => d.label); + + const tspan = text.append("tspan"); + + tspan.text(d => this.truncateDate(tspan, d.label, this._text.width)); + + return; + } + + const offset = 30; + + const enter = table.selectAll("text") + .data((d) => { + let data = []; + + if (d.data.data.birth) { + data.push({ + icon: "★", + label: d.data.data.birth, + birth: true, + withImage: d.data.data.thumbnail !== "" + }); + } + + if (d.data.data.death) { + data.push({ + icon: "†", + label: d.data.data.death, + death: true, + withImage: d.data.data.thumbnail !== "" + }); + } + + return data; + }) + .enter(); + + enter + .call((g) => { + const col1 = g.append("text") + .attr("fill", "currentColor") + .attr("text-anchor", "middle") + .attr("dominant-baseline", "middle") + .attr("x", d => this.textX(d)) + // Minor offset here to better center the icon + .attr("y", (d, i) => ((this._text.y + offset) + (i === 0 ? 0 : 21))); + + col1.append("tspan") + .text(d => d.icon) + .attr("dx", (this._orientation.isDocumentRtl ? -1 : 1) * 5); + + const col2 = g.append("text") + .attr("class", "date") + .attr("text-anchor", "start") + .attr("dominant-baseline", "middle") + .attr("x", d => this.textX(d)) + .attr("y", (d, i) => ((this._text.y + offset) + (i === 0 ? 0 : 20))); + + col2.append("title") + .text(d => d.label); + + const tspan = col2.append("tspan"); + + tspan.text(d => this.truncateDate(tspan, d.label, this._text.width - (d.withImage ? this._image.width : 0) - 25)) + .attr("dx", (this._orientation.isDocumentRtl ? -1 : 1) * 15) + ; + }); + } + + /** + * Truncates a date value. + * + * @param {Object} object The D3 object containing the text value + * @param {String} date The date value to truncate + * @param {Number} availableWidth The total available width the text could take + * + * @return {String} + * + * @private + */ + truncateDate(object, date, availableWidth) + { + const fontSize = object.style("font-size"); + const fontWeight = object.style("font-weight"); + + let truncated = false; + + // Repeat removing the last char until the width matches + while ((this.measureText(date, fontSize, fontWeight) > availableWidth) && (date.length > 1)) { + // Remove last char + date = date.slice(0, -1).trim(); + truncated = true; + } + + // Remove trailing dot if present + if (date[date.length - 1] === ".") { + date = date.slice(0, -1).trim(); + } + + return truncated ? (date + "…") : date; + } + + /** + * + * @param {Object} d + * + * @return {Number} + * + * @private + */ + textX(d) + { + const xPos = this._text.x + (d.withImage ? this._image.width : 0); + + // Reverse direction of text elements for RTL layouts + return this._orientation.isDocumentRtl ? -xPos : xPos; + } + + /** + * Measures the given text and return its width depending on the used font (including size and weight). + * + * @param {String} text + * @param {String} fontSize + * @param {Number} fontWeight + * + * @returns {Number} + * + * @private + */ + measureText(text, fontSize, fontWeight = 400) + { + const fontFamily = this._svg.get().style("font-family"); + + return measureText(text, fontFamily, fontSize, fontWeight); + } +} diff --git a/resources/js/modules/lib/tree/elbow/horizontal.js b/resources/js/modules/lib/tree/elbow/horizontal.js new file mode 100644 index 0000000..c1f21d4 --- /dev/null +++ b/resources/js/modules/lib/tree/elbow/horizontal.js @@ -0,0 +1,143 @@ +/** + * This file is part of the package magicsunday/webtrees-pedigree-chart. + * + * For the full copyright and license information, please read the + * LICENSE file distributed with this source code. + */ + +import * as d3 from "../../d3"; + +/** + * Returns the path to draw the horizontal connecting lines between the profile + * boxes for Left/Right and Right/Left layout. + * + * @param {Link} link The link object + * @param {Orientation} orientation The current orientation + * + * @returns {String} + * + * Curved edges => https://observablehq.com/@bumbeishvili/curved-edges-horizontal-d3-v3-v4-v5-v6 + */ +export default function(link, orientation) +{ + const halfXOffset = orientation.xOffset / 2; + const halfYOffset = orientation.yOffset / 2; + + let sourceX = link.source.x, + sourceY = link.source.y; + + if ((typeof link.spouse !== "undefined") && (link.source.data.family === 0)) { + // For the first family, the link to the child nodes begins between + // the individual and the first spouse. + sourceX -= getFirstSpouseLinkOffset(link, orientation); + sourceY -= (link.source.y - link.spouse.y) / 2; + } else { + // For each additional family, the link to the child nodes begins at the additional spouse. + sourceX += (orientation.boxWidth / 2) * orientation.direction; + } + + // No spouse assigned to source node + if (link.source.data.data === null) { + sourceX += (orientation.boxWidth / 2) * orientation.direction; + sourceY -= (orientation.boxHeight / 2) + (halfYOffset / 2); + } + + if (link.target !== null) { + let targetX = link.target.x - (orientation.direction * ((orientation.boxWidth / 2) + halfXOffset)), + targetY = link.target.y; + + const path = d3.path(); + + // The line from source/spouse to target + path.moveTo(sourceX, sourceY); + path.lineTo(targetX, sourceY); + path.lineTo(targetX, targetY); + path.lineTo(targetX + (orientation.direction * halfXOffset), targetY); + + return path.toString(); + } + + return createLinksBetweenSpouses(link, orientation); +} + +/** + * Returns the path needed to draw the lines between each spouse. + * + * @param {Link} link The link object + * @param {Orientation} orientation The current orientation + * + * @return {String} + */ +function createLinksBetweenSpouses(link, orientation) +{ + const path = d3.path(); + + // The distance from the line to the node. Causes the line to stop or begin just before the node, + // instead of going straight to the node, so that the connection to another spouse is clearer. + const lineStartOffset = 2; + + // Precomputed half height of box + const boxHeightHalf = orientation.boxHeight / 2; + + let sourceX = link.source.x; + + // Handle multiple spouses + if (link.spouse.data.spouses.length >= 0) { + sourceX -= getFirstSpouseLinkOffset(link, orientation); + } + + // Add a link between first spouse and source + if (link.coords === null) { + path.moveTo(sourceX, link.spouse.y + boxHeightHalf); + path.lineTo(sourceX, link.source.y - boxHeightHalf); + } + + // Append lines between the source and all spouses + if (link.coords && (link.coords.length > 0)) { + for (let i = 0; i < link.coords.length; ++i) { + let startY = link.spouse.y + boxHeightHalf; + let endY = link.coords[i].y - boxHeightHalf; + + if (i > 0) { + startY = link.coords[i - 1].y + boxHeightHalf; + } + + let startPosOffset = ((i > 0) ? lineStartOffset : 0); + let endPosOffset = (((i + 1) <= link.coords.length) ? lineStartOffset : 0); + + path.moveTo(sourceX, startY + startPosOffset); + path.lineTo(sourceX, endY - endPosOffset); + } + + // Add last part from previous spouse to actual spouse + path.moveTo( + sourceX, + link.coords[link.coords.length - 1].y + boxHeightHalf + lineStartOffset + ); + + path.lineTo( + sourceX, + link.source.y - boxHeightHalf + ); + } + + return path.toString(); +} + +/** + * Calculates the offset for the coordinate of the first spouse. + * + * @param {Link} link The link object + * @param {Orientation} orientation The current orientation + * + * @return {Number} + */ +function getFirstSpouseLinkOffset(link, orientation) +{ + // The distance between the connecting lines when there are multiple spouses + const spouseLineOffset = 5; + + return (link.source.data.family - Math.ceil(link.spouse.data.spouses.length / 2)) + * orientation.direction + * spouseLineOffset; +} diff --git a/resources/js/modules/lib/tree/elbow/vertical.js b/resources/js/modules/lib/tree/elbow/vertical.js new file mode 100644 index 0000000..510804f --- /dev/null +++ b/resources/js/modules/lib/tree/elbow/vertical.js @@ -0,0 +1,141 @@ +/** + * This file is part of the package magicsunday/webtrees-pedigree-chart. + * + * For the full copyright and license information, please read the + * LICENSE file distributed with this source code. + */ + +import * as d3 from "../../d3"; + +/** + * Returns the path to draw the vertical connecting lines between the profile + * boxes for Top/Bottom and Bottom/Top layout. + * + * @param {Link} link The link object + * @param {Orientation} orientation The current orientation + * + * @returns {String} + */ +export default function(link, orientation) +{ + const halfXOffset = orientation.xOffset / 2; + const halfYOffset = orientation.yOffset / 2; + + let sourceX = link.source.x, + sourceY = link.source.y; + + if ((typeof link.spouse !== "undefined") && (link.source.data.family === 0)) { + // For the first family, the link to the child nodes begins between + // the individual and the first spouse. + sourceX -= (link.source.x - link.spouse.x) / 2; + sourceY -= getFirstSpouseLinkOffset(link, orientation); + } else { + // For each additional family, the link to the child nodes begins at the additional spouse. + sourceY += (orientation.boxHeight / 2) * orientation.direction; + } + + // No spouse assigned to source node + if (link.source.data.data === null) { + sourceX -= (orientation.boxWidth / 2) + (halfXOffset / 2); + sourceY += (orientation.boxHeight / 2) * orientation.direction; + } + + if (link.target !== null) { + let targetX = link.target.x, + targetY = link.target.y - (orientation.direction * ((orientation.boxHeight / 2) + halfYOffset)); + + const path = d3.path(); + + // The line from source/spouse to target + path.moveTo(sourceX, sourceY); + path.lineTo(sourceX, targetY); + path.lineTo(targetX, targetY); + path.lineTo(targetX, targetY + (orientation.direction * halfYOffset)); + + return path.toString(); + } + + return createLinksBetweenSpouses(link, orientation); +} + +/** + * Returns the path needed to draw the lines between each spouse. + * + * @param {Link} link The link object + * @param {Orientation} orientation The current orientation + * + * @return {String} + */ +function createLinksBetweenSpouses(link, orientation) +{ + const path = d3.path(); + + // The distance from the line to the node. Causes the line to stop or begin just before the node, + // instead of going straight to the node, so that the connection to another spouse is clearer. + const lineStartOffset = 2; + + // Precomputed half width of box + const boxWidthHalf = orientation.boxWidth / 2; + + let sourceY = link.source.y; + + // Handle multiple spouses + if (link.spouse.data.spouses.length >= 0) { + sourceY -= getFirstSpouseLinkOffset(link, orientation); + } + + // Add a link between first spouse and source + if (link.coords === null) { + path.moveTo(link.spouse.x + boxWidthHalf, sourceY); + path.lineTo(link.source.x - boxWidthHalf, sourceY); + } + + // Append lines between the source and all spouses + if (link.coords && (link.coords.length > 0)) { + for (let i = 0; i < link.coords.length; ++i) { + let startX = link.spouse.x + boxWidthHalf; + let endX = link.coords[i].x - boxWidthHalf; + + if (i > 0) { + startX = link.coords[i - 1].x + boxWidthHalf; + } + + let startPosOffset = ((i > 0) ? lineStartOffset : 0); + let endPosOffset = (((i + 1) <= link.coords.length) ? lineStartOffset : 0); + + path.moveTo(startX + startPosOffset, sourceY); + path.lineTo(endX - endPosOffset, sourceY); + } + + // Add last part from previous spouse to actual spouse + path.moveTo( + link.coords[link.coords.length - 1].x + boxWidthHalf + lineStartOffset, + sourceY + ); + + path.lineTo( + link.source.x - boxWidthHalf, + sourceY + ); + } + + return path.toString(); +} + +/** + * Calculates the offset for the coordinate of the first spouse. + * + * @param {Link} link The link object + * @param {Orientation} orientation The current orientation + * + * @return {Number} + */ +function getFirstSpouseLinkOffset(link, orientation) +{ + // The distance between the connecting lines when there are multiple spouses + const spouseLineOffset = 5; + + return (link.source.data.family - Math.ceil(link.spouse.data.spouses.length / 2)) + * orientation.direction + * spouseLineOffset; +} diff --git a/resources/js/modules/lib/tree/link-drawer.js b/resources/js/modules/lib/tree/link-drawer.js new file mode 100644 index 0000000..ea1c0e7 --- /dev/null +++ b/resources/js/modules/lib/tree/link-drawer.js @@ -0,0 +1,130 @@ +/** + * This file is part of the package magicsunday/webtrees-pedigree-chart. + * + * For the full copyright and license information, please read the + * LICENSE file distributed with this source code. + */ + +/** + * The class handles the creation of the tree. + * + * @author Rico Sonntag + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-pedigree-chart/ + */ +export default class LinkDrawer +{ + /** + * Constructor. + * + * @param {Svg} svg + * @param {Configuration} configuration The configuration + */ + constructor(svg, configuration) + { + this._svg = svg; + this._configuration = configuration; + this._orientation = this._configuration.orientation; + } + + /** + * Draw the connecting lines. + * + * @param {Link[]} links Array of links + * @param {Individual} source The root object + * + * @public + */ + drawLinks(links, source) + { + this._svg.visual + .selectAll("path.link") + .data(links) + .join( + enter => this.linkEnter(enter, source), + update => this.linkUpdate(update), + exit => this.linkExit(exit, source) + ); + } + + /** + * Enter transition (new links). + * + * @param {selection} enter + * @param {Individual} source + * + * @private + */ + linkEnter(enter, source) + { + enter + .append("path") + .classed("link", true) + .attr("d", link => this._orientation.elbow(link)) + .call( + g => g.transition() + .duration(this._configuration.duration) + .attr("opacity", 1) + ); + } + + /** + * Update transition (existing links). + * + * @param {selection} update + * + * @private + */ + linkUpdate(update) + { + // TODO Enable for transitions + // update + // .call( + // g => g.transition() + // // .duration(this._configuration.duration) + // .attr("opacity", 1) + // .attr("d", (link) => { + // // link.source.x = source.x; + // // link.source.y = source.y; + // // + // // if (link.target) { + // // link.target.x = source.x; + // // link.target.y = source.y; + // // } + // + // return this._orientation.elbow(link); + // }) + // ); + } + + /** + * Exit transition (links to be removed). + * + * @param {selection} exit + * @param {Individual} source + * + * @private + */ + linkExit(exit, source) + { + // TODO Enable for transitions + // exit + // .call( + // g => g.transition() + // .duration(this._configuration.duration) + // .attr("opacity", 0) + // .attr("d", (link) => { + // // link.source.x = source.x; + // // link.source.y = source.y; + // // + // // if (link.target) { + // // link.target.x = source.x; + // // link.target.y = source.y; + // // } + // + // return this._orientation.elbow(link); + // }) + // .remove() + // ); + } +} diff --git a/resources/js/modules/lib/tree/name.js b/resources/js/modules/lib/tree/name.js new file mode 100644 index 0000000..ba49203 --- /dev/null +++ b/resources/js/modules/lib/tree/name.js @@ -0,0 +1,421 @@ +/** + * This file is part of the package magicsunday/webtrees-pedigree-chart. + * + * For the full copyright and license information, please read the + * LICENSE file distributed with this source code. + */ + +import measureText from "../chart/text/measure" +import OrientationTopBottom from "../chart/orientation/orientation-topBottom"; +import OrientationBottomTop from "../chart/orientation/orientation-bottomTop"; +import OrientationLeftRight from "../chart/orientation/orientation-leftRight"; +import OrientationRightLeft from "../chart/orientation/orientation-rightLeft"; + +/** + * The class handles the creation of the tree. + * + * @author Rico Sonntag + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-pedigree-chart/ + */ +export default class Name +{ + /** + * Constructor. + * + * @param {Svg} svg + * @param {Orientation} orientation + * @param {Image} image + * @param {Text} text + */ + constructor(svg, orientation, image, text) + { + this._svg = svg; + this._orientation = orientation; + this._image = image; + this._text = text; + } + + /** + * Add the individual names to the given parent element. + * + * @param {selection} parent The parent element to which the elements are to be attached + * + * @public + */ + appendName(parent) + { + const name = parent + .append("g") + .attr("class", "name"); + + // Top/Bottom and Bottom/Top + if ((this._orientation instanceof OrientationTopBottom) + || (this._orientation instanceof OrientationBottomTop) + ) { + const enter = name.selectAll("text") + .data(datum => [ + { + data: datum.data, + isRtl: datum.data.data.isNameRtl, + isAltRtl: datum.data.data.isAltRtl, + withImage: true + } + ]) + .enter(); + + enter + .call((g) => { + const text = g.append("text") + .attr("class", "wt-chart-box-name") + .attr("text-anchor", "middle") + .attr("direction", d => d.isRtl ? "rtl" : "ltr") + .attr("alignment-baseline", "central") + .attr("y", this._text.y - 5); + + this.addNameElements( + text, + datum => this.createNamesData(text, datum, true, false) + ); + }) + .call((g) => { + const text = g.append("text") + .attr("class", "wt-chart-box-name") + .attr("text-anchor", "middle") + .attr("direction", d => d.isRtl ? "rtl" : "ltr") + .attr("alignment-baseline", "central") + .attr("y", this._text.y + 15); + + this.addNameElements( + text, + datum => this.createNamesData(text, datum, false, true) + ); + }); + + // Add alternative name if present + enter + .filter(d => d.data.data.alternativeName !== "") + .call((g) => { + const text = g.append("text") + .attr("class", "wt-chart-box-name") + .attr("text-anchor", "middle") + .attr("direction", d => d.isAltRtl ? "rtl" : "ltr") + .attr("alignment-baseline", "central") + .attr("y", this._text.y + 37) + .classed("wt-chart-box-name-alt", true); + + this.addNameElements( + text, + datum => this.createAlternativeNamesData(text, datum) + ); + }); + + // Left/Right and Right/Left + } else { + const enter = name.selectAll("text") + .data(datum => [ + { + data: datum.data, + isRtl: datum.data.data.isNameRtl, + isAltRtl: datum.data.data.isAltRtl, + withImage: datum.data.data.thumbnail !== "" + } + ]) + .enter(); + + enter + .call((g) => { + const text = g.append("text") + .attr("class", "wt-chart-box-name") + .attr("text-anchor", (d) => { + if (d.isRtl && this._orientation.isDocumentRtl) { + return "start"; + } + + if (d.isRtl || this._orientation.isDocumentRtl) { + return "end"; + } + + return "start"; + }) + .attr("direction", d => d.isRtl ? "rtl" : "ltr") + .attr("x", d => this.textX(d)) + .attr("y", this._text.y - 10); + + this.addNameElements( + text, + datum => this.createNamesData(text, datum, true, true) + ); + }); + + // Add alternative name if present + enter + .filter(datum => datum.data.data.alternativeName !== "") + .call((g) => { + const text = g.append("text") + .attr("class", "wt-chart-box-name") + .attr("text-anchor", (d) => { + if (d.isAltRtl && this._orientation.isDocumentRtl) { + return "start"; + } + + if (d.isAltRtl || this._orientation.isDocumentRtl) { + return "end"; + } + + return "start"; + }) + .attr("direction", d => d.isAltRtl ? "rtl" : "ltr") + .attr("x", d => this.textX(d)) + .attr("y", this._text.y + 8) + .classed("wt-chart-box-name-alt", true); + + this.addNameElements( + text, + datum => this.createAlternativeNamesData(text, datum) + ); + }); + } + } + + /** + * Creates a single element for each single name and append it to the + * parent element. The "tspan" element containing the preferred name gets an + * additional underline style to highlight this one. + * + * @param {selection} parent The parent element to which the elements are to be attached + * @param {function(*): LabelElementData[]} data + * + * @private + */ + addNameElements(parent, data) + { + parent.selectAll("tspan") + .data(data) + .enter() + .call((g) => { + g.append("tspan") + .text(datum => datum.label) + // Add some spacing between the elements + .attr("dx", (datum, index) => { + return index !== 0 ? ((datum.isNameRtl ? -1 : 1) * 0.25) + "em" : null; + }) + // Highlight the preferred and last name + .classed("preferred", datum => datum.isPreferred) + .classed("lastName", datum => datum.isLastName); + }); + } + + /** + * Creates the data array for the names. + * + * @param {Object} parent + * @param {NameElementData} datum + * @param {Boolean} addFirstNames + * @param {Boolean} addLastNames + * + * @return {LabelElementData[]} + * + * @private + */ + createNamesData(parent, datum, addFirstNames, addLastNames) + { + /** @var {LabelElementData[]} names */ + let names = []; + + if (addFirstNames === true) { + names = names.concat( + datum.data.data.firstNames.map((firstName) => { + return { + label: firstName, + isPreferred: firstName === datum.data.data.preferredName, + isLastName: false, + isNameRtl: datum.data.data.isNameRtl + } + }) + ); + } + + if (addLastNames === true) { + // Append the last names + names = names.concat( + datum.data.data.lastNames.map((lastName) => { + return { + label: lastName, + isPreferred: false, + isLastName: true, + isNameRtl: datum.data.data.isNameRtl + } + }) + ); + } + + // // If both first and last names are empty, add the full name as an alternative + // if (!datum.data.data.firstNames.length + // && !datum.data.data.lastNames.length + // ) { + // names = names.concat([{ + // label: datum.data.data.name, + // isPreferred: false, + // isLastName: false + // }]); + // } + + const fontSize = parent.style("font-size"); + const fontWeight = parent.style("font-weight"); + + // The total available width that the text can occupy + let availableWidth = this._text.width; + + if (datum.withImage) { + if ((this._orientation instanceof OrientationLeftRight) + || (this._orientation instanceof OrientationRightLeft) + ) { + availableWidth -= this._image.width; + } + } + + return this.truncateNames(names, fontSize, fontWeight, availableWidth); + } + + /** + * Creates the data array for the alternative name. + * + * @param {Object} parent + * @param {NameElementData} datum + * + * @return {LabelElementData[]} + * + * @private + */ + createAlternativeNamesData(parent, datum) + { + let words = datum.data.data.alternativeName.split(/\s+/); + + /** @var {LabelElementData[]} names */ + let names = []; + + // Append the alternative names + names = names.concat( + words.map((word) => { + return { + label: word, + isPreferred: false, + isLastName: false, + isNameRtl: datum.data.data.isAltRtl + } + }) + ); + + const fontSize = parent.style("font-size"); + const fontWeight = parent.style("font-weight"); + + // The total available width that the text can occupy + let availableWidth = this._text.width; + + if (datum.withImage) { + if ((this._orientation instanceof OrientationLeftRight) + || (this._orientation instanceof OrientationRightLeft) + ) { + availableWidth -= this._image.width; + } + } + + return this.truncateNames(names, fontSize, fontWeight, availableWidth); + } + + /** + * Truncates the list of names. + * + * @param {LabelElementData[]} names The names array + * @param {String} fontSize The font size + * @param {Number} fontWeight The font weight + * @param {Number} availableWidth The available width + * + * @return {LabelElementData[]} + * + * @private + */ + truncateNames(names, fontSize, fontWeight, availableWidth) + { + let text = names.map(item => item.label).join(" "); + + return names + // Start truncating from the last element to the first one + .reverse() + .map((name) => { + // Select all not preferred and not last names + if ((name.isPreferred === false) + && (name.isLastName === false) + ) { + if (this.measureText(text, fontSize, fontWeight) > availableWidth) { + // Keep only the first letter + name.label = name.label.slice(0, 1) + "."; + text = names.map(item => item.label).join(" "); + } + } + + return name; + }) + .map((name) => { + // Afterward, the preferred ones if text takes still too much space + if (name.isPreferred === true) { + if (this.measureText(text, fontSize, fontWeight) > availableWidth) { + // Keep only the first letter + name.label = name.label.slice(0, 1) + "."; + text = names.map(item => item.label).join(" "); + } + } + + return name; + }) + .map((name) => { + // Finally truncate lastnames + if (name.isLastName === true) { + if (this.measureText(text, fontSize, fontWeight) > availableWidth) { + // Keep only the first letter + name.label = name.label.slice(0, 1) + "."; + text = names.map(item => item.label).join(" "); + } + } + + return name; + }) + // Revert reversed order again + .reverse(); + } + + /** + * + * @param {Object} d + * + * @return {Number} + * + * @private + */ + textX(d) + { + const xPos = this._text.x + (d.withImage ? this._image.width : 0); + + // Reverse direction of text elements for RTL layouts + return this._orientation.isDocumentRtl ? -xPos : xPos; + } + + /** + * Measures the given text and return its width depending on the used font (including size and weight). + * + * @param {String} text + * @param {String} fontSize + * @param {Number} fontWeight + * + * @returns {Number} + * + * @private + */ + measureText(text, fontSize, fontWeight = 400) + { + const fontFamily = this._svg.get().style("font-family"); + + return measureText(text, fontFamily, fontSize, fontWeight); + } +} diff --git a/resources/js/modules/lib/tree/node-drawer.js b/resources/js/modules/lib/tree/node-drawer.js new file mode 100644 index 0000000..3133718 --- /dev/null +++ b/resources/js/modules/lib/tree/node-drawer.js @@ -0,0 +1,263 @@ +/** + * This file is part of the package magicsunday/webtrees-pedigree-chart. + * + * For the full copyright and license information, please read the + * LICENSE file distributed with this source code. + */ + +import {SEX_FEMALE, SEX_MALE} from "../constants"; +import * as d3 from "../d3"; +import dataUrl from "../common/dataUrl"; +import Name from "./name"; +import Date from "./date"; +import Image from "../chart/box/image"; +import Text from "../chart/box/text"; + +/** + * The class handles the creation of the tree. + * + * @author Rico Sonntag + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-pedigree-chart/ + */ +export default class NodeDrawer +{ + /** + * Constructor. + * + * @param {Svg} svg + * @param {Hierarchy} hierarchy The hierarchical data + * @param {Configuration} configuration The configuration + */ + constructor(svg, hierarchy, configuration) + { + this._svg = svg; + this._hierarchy = hierarchy; + this._configuration = configuration; + this._orientation = this._configuration.orientation; + + this._image = new Image(this._orientation, 20); + this._text = new Text(this._orientation, this._image); + this._name = new Name(this._svg, this._orientation, this._image, this._text); + this._date = new Date(this._svg, this._orientation, this._image, this._text); + } + + /** + * Draw the person boxes. + * + * @param {Array} nodes Array of descendant nodes + * @param {Object} source The root object + * + * @public + */ + drawNodes(nodes, source) + { + // Image clip path + this._svg + .defs + .get() + .append("clipPath") + .attr("id", "clip-image") + .append("rect") + .attr("rx", this._image.rx) + .attr("ry", this._image.ry) + .attr("x", this._image.x) + .attr("y", this._image.y) + .attr("width", this._image.width) + .attr("height", this._image.height); + + this._svg.visual + .selectAll("g.person") + .data(nodes, person => person.id) + .join( + enter => this.nodeEnter(enter, source), + update => this.nodeUpdate(update), + exit => this.nodeExit(exit, source) + ); + + // this.centerTree(); + + // Stash the old positions for transition + this._hierarchy.root.eachBefore(d => { + d.x0 = d.x; + d.y0 = d.y; + }); + } + + /** + * Enter transition (new nodes). + * + * @param {selection} enter + * @param {Individual} source + * + * @private + */ + nodeEnter(enter, source) + { + enter + .append("g") + .attr("opacity", 0) + .attr("class", person => "person" + (person.data.spouse ? " spouse" : "")) + .attr("transform", (person) => { + return "translate(" + (person.x) + "," + (person.y) + ")"; + // TODO Enable this to zoom from source to person + // return "translate(" + (source.x0) + "," + (source.y0) + ")"; + }) + // TODO Enable this to collapse/expand node on click + // .on("click", (event, d) => this.togglePerson(event, d)) + .call( + // Draw the actual person rectangle with an opacity of 0.5 + g => { + g.append("rect") + .attr( + "class", + person => (person.data.data.sex === SEX_FEMALE) + ? "female" + : (person.data.data.sex === SEX_MALE) ? "male" : "unknown" + ) + .attr("rx", 20) + .attr("ry", 20) + .attr("x", -(this._orientation.boxWidth / 2)) + .attr("y", -(this._orientation.boxHeight / 2)) + .attr("width", this._orientation.boxWidth) + .attr("height", this._orientation.boxHeight) + .attr("fill-opacity", 0.5); + + g.append("title") + .text(person => person.data.data.name); + } + ) + .call( + // Draws the node (including image, names and dates) + g => this.drawNode(g) + ) + .call( + g => g.transition() + .duration(this._configuration.duration) + // .delay(1000) + .attr("opacity", 1) + // TODO Enable this to zoom from source to person + // .attr("transform", (person) => { + // return "translate(" + (person.x) + "," + (person.y) + ")"; + // }) + ); + } + + /** + * Update transition (existing nodes). + * + * @param {selection} update + * + * @private + */ + nodeUpdate(update) + { + update + .call( + g => g.transition() + .duration(this._configuration.duration) + .attr("opacity", 1) + .attr("transform", (person) => { + return "translate(" + (person.x) + "," + (person.y) + ")"; + }) + ); + } + + /** + * Exit transition (nodes to be removed). + * + * @param {selection} exit + * @param {Individual} source + * + * @private + */ + nodeExit(exit, source) + { + exit + .call( + g => g.transition() + .duration(this._configuration.duration) + .attr("opacity", 0) + .attr("transform", () => { + // Transition exit nodes to the source's position + return "translate(" + (source.x0) + "," + (source.y0) + ")"; + }) + .remove() + ); + } + + /** + * Draws the image and text nodes. + * + * @param {selection} parent The parent element to which the elements are to be attached + * + * @private + */ + drawNode(parent) + { + const enter = parent.selectAll("g.image") + .data((d) => { + let images = []; + + if (d.data.data.thumbnail) { + images.push({ + image: d.data.data.thumbnail + }) + } + + return images; + }) + .enter(); + + const group = enter.append("g") + .attr("class", "image"); + + // Background of image (only required if thumbnail has transparency (like the silhouettes)) + group + .append("rect") + .attr("x", this._image.x) + .attr("y", this._image.y) + .attr("width", this._image.width) + .attr("height", this._image.height) + .attr("rx", this._image.rx) + .attr("ry", this._image.ry) + .attr("fill", "rgb(255, 255, 255)"); + + // The individual image + group + .append("image") + .attr("x", this._image.x) + .attr("y", this._image.y) + .attr("width", this._image.width) + .attr("height", this._image.height) + .attr("clip-path", "url(#clip-image)"); + + // Border around image + group + .append("rect") + .attr("x", this._image.x) + .attr("y", this._image.y) + .attr("width", this._image.width) + .attr("height", this._image.height) + .attr("rx", this._image.rx) + .attr("ry", this._image.ry) + .attr("fill", "none") + .attr("stroke", "rgb(200, 200, 200)") + .attr("stroke-width", 1.5); + + // Asynchronously load the images + d3.selectAll("g.image image") + .each(function (d) { + let image = d3.select(this); + + dataUrl(d.image) + .then(dataUrl => image.attr("xlink:href", dataUrl)) + .catch((exception) => { + console.error(exception); + }); + }); + + this._name.appendName(parent); + this._date.appendDate(parent); + } +} diff --git a/resources/js/modules/tree.js b/resources/js/modules/tree.js deleted file mode 100644 index ecd4325..0000000 --- a/resources/js/modules/tree.js +++ /dev/null @@ -1,908 +0,0 @@ -/** - * This file is part of the package magicsunday/webtrees-pedigree-chart. - * - * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. - */ - -import * as d3 from "./d3"; -import dataUrl from "./common/dataUrl"; -import {SEX_FEMALE, SEX_MALE} from "./constants"; -import Box from "./chart/box"; - -/** - * The class handles the creation of the tree. - * - * @author Rico Sonntag - * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 - * @link https://github.com/magicsunday/webtrees-pedigree-chart/ - */ -export default class Tree -{ - /** - * Constructor. - * - * @param {Svg} svg - * @param {Configuration} configuration The configuration - * @param {Hierarchy} hierarchy The hierarchical data - */ - constructor(svg, configuration, hierarchy) - { - this._svg = svg; - this._configuration = configuration; - this._hierarchy = hierarchy; - - this._hierarchy.root.x0 = 0; - this._hierarchy.root.y0 = 0; - - this._orientation = this._configuration.orientation; - - // Create a default box container for a person based on the selected orientation - this._box = new Box(this._orientation); - - this.draw(this._hierarchy.root); - } - - /** - * Draw the tree. - * - * @param {Object} source The root object - * - * @public - */ - draw(source) - { - let nodes = this._hierarchy.nodes.descendants(); - let links = this._hierarchy.nodes.links(); - - // // Start with only the first few generations of ancestors showing - // nodes.forEach((person) => { - // if (person.parents) { - // person.parents.forEach((child) => this.collapse(child)); - // } - // }); - - // Normalize for fixed-depth. - nodes.forEach((person) => { - this._orientation.norm(person); - }); - - this.drawLinks(links, source); - this.drawNodes(nodes, source); - - // Stash the old positions for transition. - nodes.forEach((person) => { - person.x0 = person.x; - person.y0 = person.y; - }); - } - - // /** - // * Draw the tree. - // * - // * @public - // */ - // update(source) - // { - // let nodes = this._hierarchy.nodes.descendants(); - // let links = this._hierarchy.nodes.links(); - // - // // // Start with only the first few generations of ancestors showing - // // nodes.forEach((person) => { - // // if (person.parents) { - // // person.parents.forEach((child) => this.collapse(child)); - // // } - // // }); - // - // this.drawLinks(links, source); - // this.drawNodes(nodes, source); - // - // // Stash the old positions for transition. - // nodes.forEach((person) => { - // person.x0 = person.x; - // person.y0 = person.y; - // }); - // } - - /** - * Draw the person boxes. - * - * @param {Array} nodes Array of descendant nodes - * @param {Object} source The root object - * - * @private - */ - drawNodes(nodes, source) - { - let i = 0; - let that = this; - - // Image clip path - this._svg - .defs - .get() - .append("clipPath") - .attr("id", "clip-image") - .append("rect") - .attr("rx", this._box.image.rx) - .attr("ry", this._box.image.ry) - .attr("x", this._box.image.x) - .attr("y", this._box.image.y) - .attr("width", this._box.image.width) - .attr("height", this._box.image.height); - - // let t = this._svg.visual - // .transition() - // .duration(this._configuration.duration); - - let node = this._svg.visual - .selectAll("g.person") - .data(nodes, person => person.id || (person.id = ++i)); - - let nodeEnter = node - .enter() - .append("g") - .attr("class", "person") - // Add new nodes on the right side of their child's this._box. - // They will be transitioned into their proper position. - // .attr("transform", person => { - // return "translate(" + (this._configuration.direction * (source.y0 + (this._box.width / 2))) + ',' + source.x0 + ")"; - // }) - // .attr("transform", person => { - // return "translate(" + (this._configuration.direction * (source.y + (this._box.width / 2))) + ',' + source.x + ")"; - // }) - // .attr("transform", person => `translate(${source.y0}, ${source.x0})`) - .attr("transform", person => { - return "translate(" + person.x + "," + person.y + ")"; - }) - ; - - // Draw the rectangle person boxes. Start new boxes with 0 size so that we can - // transition them to their proper size. - nodeEnter - .append("rect") - .attr("class", d => (d.data.sex === SEX_FEMALE) ? "female" : (d.data.sex === SEX_MALE) ? "male" : "unknown") - .attr("rx", this._box.rx) - .attr("ry", this._box.ry) - .attr("x", this._box.x) - .attr("y", this._box.y) - .attr("width", this._box.width) - .attr("height", this._box.height) - .attr("fill-opacity", 0.5); - - // Names and Dates - nodeEnter - .filter(d => (d.data.xref !== "")) - .each(function (d) { - let element = d3.select(this); - - element - .append("title") - .text(d => d.data.name); - - const imageUrlToLoad = that.getImageToLoad(d); - - // Check if image should be shown or hidden - that._box.showImage = !!imageUrlToLoad; - - if (that._box.showImage) { - let group = element - .append("g") - .attr("class", "image"); - - // Background of image (only required if thumbnail has transparency (like the silhouettes)) - group - .append("rect") - .attr("rx", that._box.image.rx) - .attr("ry", that._box.image.ry) - .attr("x", that._box.image.x) - .attr("y", that._box.image.y) - .attr("width", that._box.image.width) - .attr("height", that._box.image.height) - .attr("fill", "rgb(255, 255, 255)"); - - // The individual image - let image = group - .append("image") - .attr("x", that._box.image.x) - .attr("y", that._box.image.y) - .attr("width", that._box.image.width) - .attr("height", that._box.image.height) - .attr("clip-path", "url(#clip-image)"); - - dataUrl(imageUrlToLoad) - .then(dataUrl => image.attr("xlink:href", dataUrl)) - .catch((exception) => { - console.error(exception); - }); - - // Border around image - group - .append("rect") - .attr("rx", that._box.image.rx) - .attr("ry", that._box.image.ry) - .attr("x", that._box.image.x) - .attr("y", that._box.image.y) - .attr("width", that._box.image.width) - .attr("height", that._box.image.height) - .attr("fill", "none") - .attr("stroke", "rgb(200, 200, 200)") - .attr("stroke-width", 1.5); - } - - that.addNames(element, d); - that.addDates(element, d); - - that._box.showImage = true; - }); - - // // Merge the update and the enter selections - // let nodeUpdate = nodeEnter.merge(node); - // - // nodeUpdate - // .transition() - // .duration(this._configuration.duration) - // // .attr("transform", person => `translate(${person.y}, ${person.x})`); - // .attr("transform", person => { - // return "translate(" + (this._configuration.direction * person.y) + "," + person.x + ")"; - // }); - // - // // Grow boxes to their proper size - // nodeUpdate.select("rect") - // .attr("x", this._box.x) - // .attr("y", this._box.y) - // .attr("width", this._box.width) - // .attr("height", this._box.height) - // // .attr("fill-opacity", "0.5") - // // .attr({ - // // x: this._box.x, - // // y: this._box.y, - // // width: this._box.width, - // // height: this._box.height - // // }) - // ; - // - // // Move text to it's proper position - // // nodeUpdate.select("text") - // // .attr("dx", this._box.x + 10) - // // .style("fill-opacity", 1); - // - // // Remove nodes we aren't showing anymore - // let nodeExit = node - // .exit() - // .transition() - // .duration(this._configuration.duration) - // // Transition exit nodes to the source's position - // .attr("transform", person => { - // return "translate(" + (this._configuration.direction * (source.y + (this._box.width / 2))) + ',' + source.x + ")"; - // }) - // // .attr("transform", person => `translate(${source.y}, ${source.x})`) - // // .attr("transform", (d) => { - // // return "translate(" + source.y + "," + source.x + ")"; - // // }) - // .remove(); - // - // // Shrink boxes as we remove them - // nodeExit.select("rect") - // .attr("x", 0) - // .attr("y", 0) - // .attr("width", 0) - // .attr("height", 0) - // // .attr("fill-opacity", 0) - // // .attr({ - // // x: 0, - // // y: 0, - // // width: 0, - // // height: 0 - // // }) - // ; - - // Fade out the text as we remove it - // nodeExit.select("text") - // .style("fill-opacity", 0) - // .attr("dx", 0); - - - // nodeEnter - // .filter(d => (d.data.xref !== "")) - // .append("title") - // .text(d => d.data.name); - - // this.addImages(nodeEnter); - - // // Names and Dates - // nodeEnter - // .filter(d => (d.data.xref !== "")) - // .each(function (d) { - // let parent = d3.select(this); - // - // // Names - // let text1 = parent - // .append("text") - // .attr("dx", -(that.boxWidth / 2) + 80) - // .attr("dy", "-12px") - // .attr("text-anchor", "start") - // .attr("class", "name"); - // - // that.addNames(text1, d); - // - // // Time span - // let text2 = parent - // .append("text") - // .attr("dx", -(that.boxWidth / 2) + 80) - // .attr("dy", "10px") - // .attr("text-anchor", "start") - // .attr("class", "date"); - // - // that.addTimeSpan(text2, d); - // }); - - - // node.join( - // enter => { - // let nodeEnter = enter - // .append("g") - // .attr("class", "person") - // // .attr("transform", person => `translate(${person.y}, ${person.x})`) - // .attr("transform", person => { - // return "translate(" + (this._configuration.direction * (source.y0 + (this._box.width / 2))) + ',' + source.x0 + ")"; - // }) - // .on("click", this.togglePerson.bind(this)); - // - // nodeEnter - // .append("rect") - // // .attr("x", this._box.x) - // // .attr("y", this._box.y) - // // .attr("width", this._box.width) - // // .attr("height", this._box.height); - // .attr("x", 0) - // .attr("y", 0) - // .attr("width", 0) - // .attr("height", 0); - // - // return nodeEnter; - // }, - // - // update => { - // let nodeUpdate = update - // .call(update => update - // .transition(t) - // .attr("transform", person => { - // return "translate(" + (this._configuration.direction * person.y) + "," + person.x + ")"; - // }) - // ); - // - // nodeUpdate - // .select("rect") - // .attr("x", this._box.x) - // .attr("y", this._box.y) - // .attr("width", this._box.width) - // .attr("height", this._box.height); - // - // return nodeUpdate; - // }, - // - // exit => { - // let nodeExit = exit - // .call(exit => exit - // .transition(t) - // .attr("transform", person => { - // return "translate(" + (this._configuration.direction * (source.y + (this._box.width / 2))) + ',' + source.x + ")"; - // }) - // ) - // .remove(); - // - // nodeExit - // .select("rect") - // .attr("x", 0) - // .attr("y", 0) - // .attr("width", 0) - // .attr("height", 0); - // - // return nodeExit; - // } - // ) - // // .selectAll("rect") - // // .attr("x", this._box.x) - // // .attr("y", this._box.y) - // // .attr("width", this._box.width) - // // .attr("height", this._box.height); - // ; - // - // return; - - } - - /** - * Update a person's state when they are clicked. - */ - togglePerson(event, person) - { - if (person.parents) { - person._parents = person.parents; - person.parents = null; - } else { - person.parents = person._parents; - person._parents = null; - } - - this.draw(person); - - // if (person.collapsed) { - // person.collapsed = false; - // } else { - // this.collapse(person); - // } - // - // this.draw(person); - } - - /** - * Collapse person (hide their ancestors). We recursively collapse the ancestors so that when the person is - * expanded it will only reveal one generation. If we don't recursively collapse the ancestors then when - * the person is clicked on again to expand, all ancestors that were previously showing will be shown again. - * If you want that behavior then just remove the recursion by removing the if block. - */ - collapse(person) - { - if (person.parents) { - person._parents = person.parents; - person._parents.forEach((child) => this.collapse(child)); - // person._parents.forEach(this.collapse); - person.parents = null; - } - - // person.collapsed = true; - // - // if (person.parents) { - // person.parents.forEach((child) => this.collapse(child)); - // person.parents.forEach(this.collapse); - // } - } - - /** - * Creates a single element for each single given name and append it to the - * parent element. The "tspan" element containing the preferred name gets an - * additional underline style in order to highlight this one. - * - * @param {selection} parent The parent ( or ) element to which the elements are to be attached - * @param {Object} datum The D3 data object containing the individual data - */ - addFirstNames(parent, datum) - { - let i = 0; - - for (let firstName of datum.data.firstNames) { - // Create a element for each given name - let tspan = parent.append("tspan") - .text(firstName); - - // The preferred name - if (firstName === datum.data.preferredName) { - tspan.attr("class", "preferred"); - } - - // Add some spacing between the elements - if (i !== 0) { - tspan.attr("dx", "0.25em"); - } - - ++i; - } - } - - /** - * Creates a single element for each last name and append it to the parent element. - * - * @param {selection} parent The parent ( or ) element to which the elements are to be attached - * @param {Object} datum The D3 data object containing the individual data - * @param {Number} dx Additional space offset to add between names - */ - addLastNames(parent, datum, dx = 0) - { - let i = 0; - - for (let lastName of datum.data.lastNames) { - // Create a element for each last name - let tspan = parent.append("tspan") - .attr("class", "lastName") - .text(lastName); - - // Add some spacing between the elements - if (i !== 0) { - tspan.attr("dx", "0.25em"); - } - - if (dx !== 0) { - tspan.attr("dx", dx + "em"); - } - - ++i; - } - } - - /** - * Loops over the elements and truncates the contained texts. - * - * @param {selection} parent The parent ( or ) element to which the elements are attached - */ - truncateNames(parent) - { - // The total available width that the text can occupy - let availableWidth = this._box.text.width; - - // Select all not preferred and not last names - // Start truncating from last element to the first one - parent.selectAll("tspan:not(.preferred):not(.lastName)") - .nodes() - .reverse() - .forEach(element => - d3.select(element) - .each(this.truncateText(parent, availableWidth)) - ); - - // Afterwards the preferred ones if text takes still too much space - parent.selectAll("tspan.preferred") - .each(this.truncateText(parent, availableWidth)); - - // Truncate lastnames - parent.selectAll("tspan.lastName") - .each(this.truncateText(parent, availableWidth)); - } - - /** - * Truncates the textual content of the actual element. - * - * @param {selection} parent The parent ( or ) element containing the child elements - * @param {Number} availableWidth The total available width the text could take - */ - truncateText(parent, availableWidth) - { - let that = this; - - return function () { - let textLength = that.getTextLength(parent); - let tspan = d3.select(this); - let words = tspan.text().split(/\s+/); - - // If the contains multiple words split them until available width matches - for (let i = words.length - 1; i >= 0; --i) { - if (textLength > availableWidth) { - // Keep only the first letter - words[i] = words[i].slice(0, 1) + "."; - - tspan.text(words.join(" ")); - - // Recalculate text length - textLength = that.getTextLength(parent); - } - } - }; - } - - /** - * Truncates a date value. - * - * @param {selection} parent The parent ( or ) element containing the child elements - * @param {Number} availableWidth The total available width the text could take - */ - truncateDate(parent, availableWidth) - { - let that = this; - - return function () { - let textLength = that.getTextLength(parent); - let tspan = d3.select(this); - let text = tspan.text(); - - // Repeat removing the last char until the width matches - while ((textLength > availableWidth) && (text.length > 1)) { - // Remove last char - text = text.slice(0, -1).trim(); - - tspan.text(text); - - // Recalculate text length - textLength = that.getTextLength(parent); - } - - // Remove trailing dot if present - if (text[text.length - 1] === ".") { - tspan.text(text.slice(0, -1).trim()); - } - }; - } - - /** - * Returns a float representing the computed length of all elements within the element. - * - * @param {selection} parent The parent ( or ) element containing the child elements - * - * @returns {Number} - */ - getTextLength(parent) - { - let totalWidth = 0; - - // Calculate the total used width of all elements - parent.selectAll("tspan").each(function () { - totalWidth += this.getComputedTextLength(); - }); - - return totalWidth; - } - - /** - * Add the individual names to the given parent element. - * - * @param {selection} parent The parent element to which the elements are to be attached - * @param {Object} datum The D3 data object - */ - addNames(parent, datum) - { - let name = parent - .append("g") - .attr("class", "name"); - - // Top/Bottom and Bottom/Top - if (this._orientation.splittNames) { - let text1 = name.append("text") - .attr("class", "wt-chart-box-name") - .attr("text-anchor", "middle") - .attr("alignment-baseline", "central") - .attr("dy", this._box.text.y); - - let text2 = name.append("text") - .attr("class", "wt-chart-box-name") - .attr("text-anchor", "middle") - .attr("alignment-baseline", "central") - .attr("dy", this._box.text.y + 20); - - this.addFirstNames(text1, datum); - this.addLastNames(text2, datum); - - // If both first and last names are empty, add the full name as an alternative - if (!datum.data.firstNames.length - && !datum.data.lastNames.length - ) { - text1.append("tspan") - .text(datum.data.name); - } - - this.truncateNames(text1); - this.truncateNames(text2); - - // Left/Right and Right/Left - } else { - let text1 = name.append("text") - .attr("class", "wt-chart-box-name") - .attr("text-anchor", this._configuration.rtl ? "end" : "start") - .attr("dx", this._box.text.x) - .attr("dy", this._box.text.y); - - this.addFirstNames(text1, datum); - this.addLastNames(text1, datum, 0.25); - - // If both first and last names are empty, add the full name as an alternative - if (!datum.data.firstNames.length - && !datum.data.lastNames.length - ) { - text1.append("tspan") - .text(datum.data.name); - } - - this.truncateNames(text1); - } - } - - /** - * Add the individual dates to the given parent element. - * - * @param {selection} parent The parent element to which the elements are to be attached - * @param {Object} datum The D3 data object - */ - addDates(parent, datum) - { - let table = parent - .append("g") - .attr("class", "table"); - - // Top/Bottom and Bottom/Top - if (this._orientation.splittNames) { - let text = table.append("text") - .attr("class", "date") - .attr("text-anchor", "middle") - .attr("alignment-baseline", "central") - .attr("dy", this._box.text.y + 50); - - text.append("title") - .text(datum.data.timespan); - - let tspan = text.append("tspan") - .text(datum.data.timespan); - - if (this.getTextLength(text) > this._box.text.width) { - text.selectAll("tspan") - .each(this.truncateDate(text, this._box.text.width)); - - tspan.text(tspan.text() + "\u2026"); - } - - return; - } - - let offset = 20; - - if (datum.data.birth) { - let col1 = table - .append("text") - .attr("fill", "currentColor") - .attr("text-anchor", "middle") - .attr("dominant-baseline", "middle") - .attr("x", this._box.text.x) - .attr("dy", this._box.text.y + offset); - - col1.append("tspan") - .text("\u2605") - .attr("x", this._box.text.x + 5); - - let col2 = table - .append("text") - .attr("class", "date") - .attr("text-anchor", this._configuration.rtl ? "end" : "start") - .attr("dominant-baseline", "middle") - .attr("x", this._box.text.x) - .attr("dy", this._box.text.y + offset); - - col2.append("title") - .text(datum.data.birth); - - let tspan = col2 - .append("tspan") - .text(datum.data.birth) - .attr("x", this._box.text.x + 15); - - if (this.getTextLength(col2) > (this._box.text.width - 25)) { - col2.selectAll("tspan") - .each(this.truncateDate(col2, this._box.text.width - 25)); - - tspan.text(tspan.text() + "\u2026"); - } - } - - if (datum.data.death) { - if (datum.data.birth) { - offset += 20; - } - - let col1 = table - .append("text") - .attr("fill", "currentColor") - .attr("text-anchor", "middle") - .attr("dominant-baseline", "middle") - .attr("x", this._box.text.x) - .attr("dy", this._box.text.y + offset); - - col1.append("tspan") - .text("\u2020") - .attr("x", this._box.text.x + 5); - - let col2 = table - .append("text") - .attr("class", "date") - .attr("text-anchor", this._configuration.rtl ? "end" : "start") - .attr("dominant-baseline", "middle") - .attr("x", this._box.text.x) - .attr("dy", this._box.text.y + offset); - - col2.append("title") - .text(datum.data.death); - - let tspan = col2 - .append("tspan") - .text(datum.data.death) - .attr("x", this._box.text.x + 15); - - if (this.getTextLength(col2) > (this._box.text.width - 25)) { - col2.selectAll("tspan") - .each(this.truncateDate(col2, this._box.text.width - 25)); - - tspan.text(tspan.text().trim() + "\u2026"); - } - } - } - - /** - * Return the image file or the placeholder. - * - * @param {Object} datum The D3 data object - * - * @returns {String} - */ - getImageToLoad(datum) - { - if (datum.data.thumbnail) { - return datum.data.thumbnail; - } - - return ""; - } - - /** - * Draw the connecting lines. - * - * @param {Link[]} links Array of links - * @param {Object} source The root object - * - * @private - */ - drawLinks(links, source) - { - let linkPath = this._svg.visual - .selectAll("path.link") - .data(links); //, person => person.target.id); - - // Add new links. Transition new links from the source's old position to - // the links final position. - let linkEnter = linkPath - .enter() - .append("path") - .classed("link", true) - .attr("d", link => this._orientation.elbow(link)); - - - // // Add new links. Transition new links from the source's old position to - // // the links final position. - // let linkEnter = link.enter() - // .append("path") - // .classed("link", true) - // .attr("d", person => { - // const o = { - // x: source.x0, - // y: this._configuration.direction * (source.y0 + (this._box.width / 2)) - // }; - // - // return this.transitionElbow({ source: o, target: o }); - // }); - // - // var linkUpdate = linkEnter.merge(link); - // - // // Update the old links positions - // linkUpdate.transition() - // .duration(this._configuration.duration) - // .attr("d", person => this.elbow(person)); - // - // // Remove any links we don't need anymore if part of the tree was collapsed. Transition exit - // // links from their current position to the source's new position. - // link.exit() - // .transition() - // .duration(this._configuration.duration) - // .attr("d", person => { - // const o = { - // x: source.x, - // y: this._configuration.direction * (source.y + this._box.width / 2) - // }; - // - // return this.transitionElbow({ source: o, target: o }); - // }) - // .remove(); - } - - // /** - // * Use a different elbow function for enter - // * and exit nodes. This is necessary because - // * the function above assumes that the nodes - // * are stationary along the x axis. - // * - // * @param {Object} datum D3 data object - // * - // * @private - // */ - // transitionElbow(datum) - // { - // return "M" + datum.source.y + "," + datum.source.x - // + "H" + datum.source.y - // + "V" + datum.source.x - // + "H" + datum.source.y; - // } -} diff --git a/resources/js/pedigree-chart.min.js b/resources/js/pedigree-chart.min.js index 8090459..7e8d5af 100644 --- a/resources/js/pedigree-chart.min.js +++ b/resources/js/pedigree-chart.min.js @@ -1 +1 @@ -var t,e;t=this,e=function(t){function e(t){if(!t.ok)throw new Error(t.status+" "+t.statusText);return t.text()}function n(t){var e=0,n=t.children,r=n&&n.length;if(r)for(;--r>=0;)e+=n[r].value;else e=1;t.value=e}function r(t,e){t instanceof Map?(t=[void 0,t],void 0===e&&(e=o)):void 0===e&&(e=i);for(var n,r,a,u,l,c=new h(t),f=[c];n=f.pop();)if((a=e(n.data))&&(l=(a=Array.from(a)).length))for(n.children=a,u=l-1;u>=0;--u)f.push(r=a[u]=new h(a[u])),r.parent=n,r.depth=n.depth+1;return c.eachBefore(s)}function i(t){return t.children}function o(t){return Array.isArray(t)?t[1]:null}function a(t){void 0!==t.data.value&&(t.value=t.data.value),t.data=t.data.data}function s(t){var e=0;do{t.height=e}while((t=t.parent)&&t.height<++e)}function h(t){this.data=t,this.depth=this.height=0,this.parent=null}function u(t,e){return t.parent===e.parent?1:2}function l(t){var e=t.children;return e?e[0]:t.t}function c(t){var e=t.children;return e?e[e.length-1]:t.t}function f(t,e,n){var r=n/(e.i-t.i);e.c-=r,e.s+=n,t.c+=r,e.z+=n,e.m+=n}function p(t,e,n){return t.a.parent===e.parent?t.a:n}function d(t,e){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=e}function g(){var t=u,e=1,n=1,r=null;function i(i){var h=function(t){for(var e,n,r,i,o,a=new d(t,0),s=[a];e=s.pop();)if(r=e._.children)for(e.children=new Array(o=r.length),i=o-1;i>=0;--i)s.push(n=e.children[i]=new d(r[i],i)),n.parent=e;return(a.parent=new d(null,0)).children=[a],a}(i);if(h.eachAfter(o),h.parent.m=-h.z,h.eachBefore(a),r)i.eachBefore(s);else{var u=i,l=i,c=i;i.eachBefore((function(t){t.xl.x&&(l=t),t.depth>c.depth&&(c=t)}));var f=u===l?1:t(u,l)/2,p=f-u.x,g=e/(l.x+f+p),_=n/(c.depth||1);i.eachBefore((function(t){t.x=(t.x+p)*g,t.y=t.depth*_}))}return i}function o(e){var n=e.children,r=e.parent.children,i=e.i?r[e.i-1]:null;if(n){!function(t){for(var e,n=0,r=0,i=t.children,o=i.length;--o>=0;)(e=i[o]).z+=n,e.m+=n,n+=e.s+(r+=e.c)}(e);var o=(n[0].z+n[n.length-1].z)/2;i?(e.z=i.z+t(e._,i._),e.m=e.z-o):e.z=o}else i&&(e.z=i.z+t(e._,i._));e.parent.A=function(e,n,r){if(n){for(var i,o=e,a=e,s=n,h=o.parent.children[0],u=o.m,d=a.m,g=s.m,_=h.m;s=c(s),o=l(o),s&&o;)h=l(h),(a=c(a)).a=e,(i=s.z+g-o.z-u+t(s._,o._))>0&&(f(p(s,e,r),e,i),u+=i,d+=i),g+=s.m,u+=o.m,_+=h.m,d+=a.m;s&&!c(a)&&(a.t=s,a.m+=g-d),o&&!l(h)&&(h.t=o,h.m+=u-_,r=e)}return r}(e,i,e.parent.A||r[0])}function a(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function s(t){t.x*=e,t.y=t.depth*n}return i.separation=function(e){return arguments.length?(t=e,i):t},i.size=function(t){return arguments.length?(r=!1,e=+t[0],n=+t[1],i):r?null:[e,n]},i.nodeSize=function(t){return arguments.length?(r=!0,e=+t[0],n=+t[1],i):r?[e,n]:null},i}h.prototype=r.prototype={constructor:h,count:function(){return this.eachAfter(n)},each:function(t,e){let n=-1;for(const r of this)t.call(e,r,++n,this);return this},eachAfter:function(t,e){for(var n,r,i,o=this,a=[o],s=[],h=-1;o=a.pop();)if(s.push(o),n=o.children)for(r=0,i=n.length;r=0;--r)o.push(n[r]);return this},find:function(t,e){let n=-1;for(const r of this)if(t.call(e,r,++n,this))return r},sum:function(t){return this.eachAfter((function(e){for(var n=+t(e.data)||0,r=e.children,i=r&&r.length;--i>=0;)n+=r[i].value;e.value=n}))},sort:function(t){return this.eachBefore((function(e){e.children&&e.children.sort(t)}))},path:function(t){for(var e=this,n=function(t,e){if(t===e)return t;var n=t.ancestors(),r=e.ancestors(),i=null;for(t=n.pop(),e=r.pop();t===e;)i=t,t=n.pop(),e=r.pop();return i}(e,t),r=[e];e!==n;)e=e.parent,r.push(e);for(var i=r.length;t!==n;)r.splice(i,0,t),t=t.parent;return r},ancestors:function(){for(var t=this,e=[t];t=t.parent;)e.push(t);return e},descendants:function(){return Array.from(this)},leaves:function(){var t=[];return this.eachBefore((function(e){e.children||t.push(e)})),t},links:function(){var t=this,e=[];return t.each((function(n){n!==t&&e.push({source:n.parent,target:n})})),e},copy:function(){return r(this).eachBefore(a)},[Symbol.iterator]:function*(){var t,e,n,r,i=this,o=[i];do{for(t=o.reverse(),o=[];i=t.pop();)if(yield i,e=i.children)for(n=0,r=e.length;n=0))throw new Error(`invalid digits: ${t}`);if(e>15)return x;const n=10**e;return function(t){this._+=t[0];for(let e=1,r=t.length;ey)if(Math.abs(l*s-h*u)>y&&i){let f=n-o,p=r-a,d=s*s+h*h,g=f*f+p*p,m=Math.sqrt(d),v=Math.sqrt(c),x=i*Math.tan((_-Math.acos((d+c-g)/(2*m*v)))/2),w=x/v,b=x/m;Math.abs(w-1)>y&&this._append`L${t+w*u},${e+w*l}`,this._append`A${i},${i},0,0,${+(l*f>u*p)},${this._x1=t+b*s},${this._y1=e+b*h}`}else this._append`L${this._x1=t},${this._y1=e}`}arc(t,e,n,r,i,o){if(t=+t,e=+e,o=!!o,(n=+n)<0)throw new Error(`negative radius: ${n}`);let a=n*Math.cos(r),s=n*Math.sin(r),h=t+a,u=e+s,l=1^o,c=o?r-i:i-r;null===this._x1?this._append`M${h},${u}`:(Math.abs(this._x1-h)>y||Math.abs(this._y1-u)>y)&&this._append`L${h},${u}`,n&&(c<0&&(c=c%m+m),c>v?this._append`A${n},${n},0,1,${l},${t-a},${e-s}A${n},${n},0,1,${l},${this._x1=h},${this._y1=u}`:c>y&&this._append`A${n},${n},0,${+(c>=_)},${l},${this._x1=t+n*Math.cos(i)},${this._y1=e+n*Math.sin(i)}`)}rect(t,e,n,r){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+e}h${n=+n}v${+r}h${-n}Z`}toString(){return this._}}function b(){return new w}b.prototype=w.prototype;var k="http://www.w3.org/1999/xhtml",N={svg:"http://www.w3.org/2000/svg",xhtml:k,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function A(t){var e=t+="",n=e.indexOf(":");return n>=0&&"xmlns"!==(e=t.slice(0,n))&&(t=t.slice(n+1)),N.hasOwnProperty(e)?{space:N[e],local:t}:t}function M(t){return function(){var e=this.ownerDocument,n=this.namespaceURI;return n===k&&e.documentElement.namespaceURI===k?e.createElement(t):e.createElementNS(n,t)}}function $(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function z(t){var e=A(t);return(e.local?$:M)(e)}function E(){}function T(t){return null==t?E:function(){return this.querySelector(t)}}function S(){return[]}function P(t){return null==t?S:function(){return this.querySelectorAll(t)}}function C(t){return function(){return null==(e=t.apply(this,arguments))?[]:Array.isArray(e)?e:Array.from(e);var e}}function L(t){return function(){return this.matches(t)}}function R(t){return function(e){return e.matches(t)}}var I=Array.prototype.find;function B(){return this.firstElementChild}var D=Array.prototype.filter;function X(){return Array.from(this.children)}function H(t){return new Array(t.length)}function W(t,e){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=e}function q(t,e,n,r,i,o){for(var a,s=0,h=e.length,u=o.length;se?1:t>=e?0:NaN}function j(t){return function(){this.removeAttribute(t)}}function F(t){return function(){this.removeAttributeNS(t.space,t.local)}}function G(t,e){return function(){this.setAttribute(t,e)}}function K(t,e){return function(){this.setAttributeNS(t.space,t.local,e)}}function Q(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttribute(t):this.setAttribute(t,n)}}function Z(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,n)}}function J(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function tt(t){return function(){this.style.removeProperty(t)}}function et(t,e,n){return function(){this.style.setProperty(t,e,n)}}function nt(t,e,n){return function(){var r=e.apply(this,arguments);null==r?this.style.removeProperty(t):this.style.setProperty(t,r,n)}}function rt(t,e){return t.style.getPropertyValue(e)||J(t).getComputedStyle(t,null).getPropertyValue(e)}function it(t){return function(){delete this[t]}}function ot(t,e){return function(){this[t]=e}}function at(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function st(t){return t.trim().split(/^|\s+/)}function ht(t){return t.classList||new ut(t)}function ut(t){this._node=t,this._names=st(t.getAttribute("class")||"")}function lt(t,e){for(var n=ht(t),r=-1,i=e.length;++r=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var Pt=[null];function Ct(t,e){this._groups=t,this._parents=e}function Lt(){return new Ct([[document.documentElement]],Pt)}function Rt(t){return"string"==typeof t?new Ct([[document.querySelector(t)]],[document.documentElement]):new Ct([[t]],Pt)}function It(t,e){if(t=function(t){let e;for(;e=t.sourceEvent;)t=e;return t}(t),void 0===e&&(e=t.currentTarget),e){var n=e.ownerSVGElement||e;if(n.createSVGPoint){var r=n.createSVGPoint();return r.x=t.clientX,r.y=t.clientY,[(r=r.matrixTransform(e.getScreenCTM().inverse())).x,r.y]}if(e.getBoundingClientRect){var i=e.getBoundingClientRect();return[t.clientX-i.left-e.clientLeft,t.clientY-i.top-e.clientTop]}}return[t.pageX,t.pageY]}Ct.prototype=Lt.prototype={constructor:Ct,select:function(t){"function"!=typeof t&&(t=T(t));for(var e=this._groups,n=e.length,r=new Array(n),i=0;i=w&&(w=x+1);!(v=m[w])&&++w=0;)(r=i[o])&&(a&&4^r.compareDocumentPosition(a)&&a.parentNode.insertBefore(r,a),a=r);return this},sort:function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=U);for(var n=this._groups,r=n.length,i=new Array(r),o=0;o1?this.each((null==e?tt:"function"==typeof e?nt:et)(t,e,null==n?"":n)):rt(this.node(),t)},property:function(t,e){return arguments.length>1?this.each((null==e?it:"function"==typeof e?at:ot)(t,e)):this.node()[t]},classed:function(t,e){var n=st(t+"");if(arguments.length<2){for(var r=ht(this.node()),i=-1,o=n.length;++i=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}}))}(t+""),a=o.length;if(!(arguments.length<2)){for(s=e?zt:$t,r=0;r{}};function Dt(){for(var t,e=0,n=arguments.length,r={};e=0&&(e=t.slice(n+1),t=t.slice(0,n)),t&&!r.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}}))),a=-1,s=o.length;if(!(arguments.length<2)){if(null!=e&&"function"!=typeof e)throw new Error("invalid callback: "+e);for(;++a0)for(var n,r,i=new Array(n),o=0;o=0&&e._call.call(void 0,t),e=e._next;--Ot}()}finally{Ot=0,function(){for(var t,e,n=qt,r=1/0;n;)n._call?(r>n._time&&(r=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:qt=e);Yt=t,oe(r)}(),Gt=0}}function ie(){var t=Qt.now(),e=t-Ft;e>jt&&(Kt-=e,Ft=t)}function oe(t){Ot||(Vt&&(Vt=clearTimeout(Vt)),t-Gt>24?(t<1/0&&(Vt=setTimeout(re,t-Qt.now()-Kt)),Ut&&(Ut=clearInterval(Ut))):(Ut||(Ft=Qt.now(),Ut=setInterval(ie,jt)),Ot=1,Zt(re)))}function ae(t,e,n){var r=new ee;return e=null==e?0:+e,r.restart((n=>{r.stop(),t(n+e)}),e,n),r}ee.prototype=ne.prototype={constructor:ee,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?Jt():+n)+(null==e?0:+e),this._next||Yt===this||(Yt?Yt._next=this:qt=this,Yt=this),this._call=t,this._time=n,oe()},stop:function(){this._call&&(this._call=null,this._time=1/0,oe())}};var se=Dt("start","end","cancel","interrupt"),he=[],ue=0,le=1,ce=2,fe=3,pe=4,de=5,ge=6;function _e(t,e,n,r,i,o){var a=t.__transition;if(a){if(n in a)return}else t.__transition={};!function(t,e,n){var r,i=t.__transition;function o(t){n.state=le,n.timer.restart(a,n.delay,n.time),n.delay<=t&&a(t-n.delay)}function a(o){var u,l,c,f;if(n.state!==le)return h();for(u in i)if((f=i[u]).name===n.name){if(f.state===fe)return ae(a);f.state===pe?(f.state=ge,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete i[u]):+uue)throw new Error("too late; already scheduled");return n}function ye(t,e){var n=ve(t,e);if(n.state>fe)throw new Error("too late; already running");return n}function ve(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function xe(t,e){var n,r,i,o=t.__transition,a=!0;if(o){for(i in e=null==e?null:e+"",o)(n=o[i]).name===e?(r=n.state>ce&&n.state>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?We(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?We(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=Te.exec(t))?new Ye(e[1],e[2],e[3],1):(e=Se.exec(t))?new Ye(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=Pe.exec(t))?We(e[1],e[2],e[3],e[4]):(e=Ce.exec(t))?We(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=Le.exec(t))?Ge(e[1],e[2]/100,e[3]/100,1):(e=Re.exec(t))?Ge(e[1],e[2]/100,e[3]/100,e[4]):Ie.hasOwnProperty(t)?He(Ie[t]):"transparent"===t?new Ye(NaN,NaN,NaN,0):null}function He(t){return new Ye(t>>16&255,t>>8&255,255&t,1)}function We(t,e,n,r){return r<=0&&(t=e=n=NaN),new Ye(t,e,n,r)}function qe(t,e,n,r){return 1===arguments.length?((i=t)instanceof ke||(i=Xe(i)),i?new Ye((i=i.rgb()).r,i.g,i.b,i.opacity):new Ye):new Ye(t,e,n,null==r?1:r);var i}function Ye(t,e,n,r){this.r=+t,this.g=+e,this.b=+n,this.opacity=+r}function Oe(){return`#${Fe(this.r)}${Fe(this.g)}${Fe(this.b)}`}function Ve(){const t=Ue(this.opacity);return`${1===t?"rgb(":"rgba("}${je(this.r)}, ${je(this.g)}, ${je(this.b)}${1===t?")":`, ${t})`}`}function Ue(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function je(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function Fe(t){return((t=je(t))<16?"0":"")+t.toString(16)}function Ge(t,e,n,r){return r<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new Qe(t,e,n,r)}function Ke(t){if(t instanceof Qe)return new Qe(t.h,t.s,t.l,t.opacity);if(t instanceof ke||(t=Xe(t)),!t)return new Qe;if(t instanceof Qe)return t;var e=(t=t.rgb()).r/255,n=t.g/255,r=t.b/255,i=Math.min(e,n,r),o=Math.max(e,n,r),a=NaN,s=o-i,h=(o+i)/2;return s?(a=e===o?(n-r)/s+6*(n0&&h<1?0:a,new Qe(a,s,h,t.opacity)}function Qe(t,e,n,r){this.h=+t,this.s=+e,this.l=+n,this.opacity=+r}function Ze(t){return(t=(t||0)%360)<0?t+360:t}function Je(t){return Math.max(0,Math.min(1,t||0))}function tn(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}we(ke,Xe,{copy(t){return Object.assign(new this.constructor,this,t)},displayable(){return this.rgb().displayable()},hex:Be,formatHex:Be,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return Ke(this).formatHsl()},formatRgb:De,toString:De}),we(Ye,qe,be(ke,{brighter(t){return t=null==t?Ae:Math.pow(Ae,t),new Ye(this.r*t,this.g*t,this.b*t,this.opacity)},darker(t){return t=null==t?Ne:Math.pow(Ne,t),new Ye(this.r*t,this.g*t,this.b*t,this.opacity)},rgb(){return this},clamp(){return new Ye(je(this.r),je(this.g),je(this.b),Ue(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:Oe,formatHex:Oe,formatHex8:function(){return`#${Fe(this.r)}${Fe(this.g)}${Fe(this.b)}${Fe(255*(isNaN(this.opacity)?1:this.opacity))}`},formatRgb:Ve,toString:Ve})),we(Qe,(function(t,e,n,r){return 1===arguments.length?Ke(t):new Qe(t,e,n,null==r?1:r)}),be(ke,{brighter(t){return t=null==t?Ae:Math.pow(Ae,t),new Qe(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=null==t?Ne:Math.pow(Ne,t),new Qe(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,r=n+(n<.5?n:1-n)*e,i=2*n-r;return new Ye(tn(t>=240?t-240:t+120,i,r),tn(t,i,r),tn(t<120?t+240:t-120,i,r),this.opacity)},clamp(){return new Qe(Ze(this.h),Je(this.s),Je(this.l),Ue(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=Ue(this.opacity);return`${1===t?"hsl(":"hsla("}${Ze(this.h)}, ${100*Je(this.s)}%, ${100*Je(this.l)}%${1===t?")":`, ${t})`}`}}));var en=t=>()=>t;function nn(t){return 1==(t=+t)?rn:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(r){return Math.pow(t+r*e,n)}}(e,n,t):en(isNaN(e)?n:e)}}function rn(t,e){var n=e-t;return n?function(t,e){return function(n){return t+n*e}}(t,n):en(isNaN(t)?e:t)}var on=function t(e){var n=nn(e);function r(t,e){var r=n((t=qe(t)).r,(e=qe(e)).r),i=n(t.g,e.g),o=n(t.b,e.b),a=rn(t.opacity,e.opacity);return function(e){return t.r=r(e),t.g=i(e),t.b=o(e),t.opacity=a(e),t+""}}return r.gamma=t,r}(1);function an(t,e){return t=+t,e=+e,function(n){return t*(1-n)+e*n}}var sn=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,hn=new RegExp(sn.source,"g");function un(t,e){var n,r,i,o=sn.lastIndex=hn.lastIndex=0,a=-1,s=[],h=[];for(t+="",e+="";(n=sn.exec(t))&&(r=hn.exec(e));)(i=r.index)>o&&(i=e.slice(o,i),s[a]?s[a]+=i:s[++a]=i),(n=n[0])===(r=r[0])?s[a]?s[a]+=r:s[++a]=r:(s[++a]=null,h.push({i:a,x:an(n,r)})),o=hn.lastIndex;return o180?e+=360:e-t>180&&(t+=360),o.push({i:n.push(i(n)+"rotate(",null,r)-2,x:an(t,e)})):e&&n.push(i(n)+"rotate("+e+r)}(o.rotate,a.rotate,s,h),function(t,e,n,o){t!==e?o.push({i:n.push(i(n)+"skewX(",null,r)-2,x:an(t,e)}):e&&n.push(i(n)+"skewX("+e+r)}(o.skewX,a.skewX,s,h),function(t,e,n,r,o,a){if(t!==n||e!==r){var s=o.push(i(o)+"scale(",null,",",null,")");a.push({i:s-4,x:an(t,n)},{i:s-2,x:an(e,r)})}else 1===n&&1===r||o.push(i(o)+"scale("+n+","+r+")")}(o.scaleX,o.scaleY,a.scaleX,a.scaleY,s,h),o=a=null,function(t){for(var e,n=-1,r=h.length;++n=0&&(t=t.slice(0,e)),!t||"start"===t}))}(e)?me:ye;return function(){var a=o(this,t),s=a.on;s!==r&&(i=(r=s).copy()).on(e,n),a.on=i}}(n,t,e))},attr:function(t,e){var n=A(t),r="transform"===n?_n:bn;return this.attrTween(t,"function"==typeof e?(n.local?zn:$n)(n,r,wn(this,"attr."+t,e)):null==e?(n.local?Nn:kn)(n):(n.local?Mn:An)(n,r,e))},attrTween:function(t,e){var n="attr."+t;if(arguments.length<2)return(n=this.tween(n))&&n._value;if(null==e)return this.tween(n,null);if("function"!=typeof e)throw new Error;var r=A(t);return this.tween(n,(r.local?En:Tn)(r,e))},style:function(t,e,n){var r="transform"==(t+="")?gn:bn;return null==e?this.styleTween(t,function(t,e){var n,r,i;return function(){var o=rt(this,t),a=(this.style.removeProperty(t),rt(this,t));return o===a?null:o===n&&a===r?i:i=e(n=o,r=a)}}(t,r)).on("end.style."+t,In(t)):"function"==typeof e?this.styleTween(t,function(t,e,n){var r,i,o;return function(){var a=rt(this,t),s=n(this),h=s+"";return null==s&&(this.style.removeProperty(t),h=s=rt(this,t)),a===h?null:a===r&&h===i?o:(i=h,o=e(r=a,s))}}(t,r,wn(this,"style."+t,e))).each(function(t,e){var n,r,i,o,a="style."+e,s="end."+a;return function(){var h=ye(this,t),u=h.on,l=null==h.value[a]?o||(o=In(e)):void 0;u===n&&i===l||(r=(n=u).copy()).on(s,i=l),h.on=r}}(this._id,t)):this.styleTween(t,function(t,e,n){var r,i,o=n+"";return function(){var a=rt(this,t);return a===o?null:a===r?i:i=e(r=a,n)}}(t,r,e),n).on("end.style."+t,null)},styleTween:function(t,e,n){var r="style."+(t+="");if(arguments.length<2)return(r=this.tween(r))&&r._value;if(null==e)return this.tween(r,null);if("function"!=typeof e)throw new Error;return this.tween(r,function(t,e,n){var r,i;function o(){var o=e.apply(this,arguments);return o!==i&&(r=(i=o)&&function(t,e,n){return function(r){this.style.setProperty(t,e.call(this,r),n)}}(t,o,n)),r}return o._value=e,o}(t,e,null==n?"":n))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var e=t(this);this.textContent=null==e?"":e}}(wn(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},textTween:function(t){var e="text";if(arguments.length<1)return(e=this.tween(e))&&e._value;if(null==t)return this.tween(e,null);if("function"!=typeof t)throw new Error;return this.tween(e,function(t){var e,n;function r(){var r=t.apply(this,arguments);return r!==n&&(e=(n=r)&&function(t){return function(e){this.textContent=t.call(this,e)}}(r)),e}return r._value=t,r}(t))},remove:function(){return this.on("end.remove",function(t){return function(){var e=this.parentNode;for(var n in this.__transition)if(+n!==t)return;e&&e.removeChild(this)}}(this._id))},tween:function(t,e){var n=this._id;if(t+="",arguments.length<2){for(var r,i=ve(this.node(),n).tween,o=0,a=i.length;o()=>t;function Un(t,{sourceEvent:e,target:n,transform:r,dispatch:i}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:e,enumerable:!0,configurable:!0},target:{value:n,enumerable:!0,configurable:!0},transform:{value:r,enumerable:!0,configurable:!0},_:{value:i}})}function jn(t,e,n){this.k=t,this.x=e,this.y=n}jn.prototype={constructor:jn,scale:function(t){return 1===t?this:new jn(this.k*t,this.x,this.y)},translate:function(t,e){return 0===t&0===e?this:new jn(this.k,this.x+this.k*t,this.y+this.k*e)},apply:function(t){return[t[0]*this.k+this.x,t[1]*this.k+this.y]},applyX:function(t){return t*this.k+this.x},applyY:function(t){return t*this.k+this.y},invert:function(t){return[(t[0]-this.x)/this.k,(t[1]-this.y)/this.k]},invertX:function(t){return(t-this.x)/this.k},invertY:function(t){return(t-this.y)/this.k},rescaleX:function(t){return t.copy().domain(t.range().map(this.invertX,this).map(t.invert,t))},rescaleY:function(t){return t.copy().domain(t.range().map(this.invertY,this).map(t.invert,t))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Fn=new jn(1,0,0);function Gn(t){for(;!t.__zoom;)if(!(t=t.parentNode))return Fn;return t.__zoom}function Kn(t){t.stopImmediatePropagation()}function Qn(t){t.preventDefault(),t.stopImmediatePropagation()}function Zn(t){return!(t.ctrlKey&&"wheel"!==t.type||t.button)}function Jn(){var t=this;return t instanceof SVGElement?(t=t.ownerSVGElement||t).hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]:[[0,0],[t.clientWidth,t.clientHeight]]}function tr(){return this.__zoom||Fn}function er(t){return-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002)*(t.ctrlKey?10:1)}function nr(){return navigator.maxTouchPoints||"ontouchstart"in this}function rr(t,e,n){var r=t.invertX(e[0][0])-n[0][0],i=t.invertX(e[1][0])-n[1][0],o=t.invertY(e[0][1])-n[0][1],a=t.invertY(e[1][1])-n[1][1];return t.translate(i>r?(r+i)/2:Math.min(0,r)||Math.max(0,i),a>o?(o+a)/2:Math.min(0,o)||Math.max(0,a))}function ir(){var t,e,n,r=Zn,i=Jn,o=rr,a=er,s=nr,h=[0,1/0],u=[[-1/0,-1/0],[1/0,1/0]],l=250,c=yn,f=Dt("start","zoom","end"),p=500,d=150,g=0,_=10;function m(t){t.property("__zoom",tr).on("wheel.zoom",N,{passive:!1}).on("mousedown.zoom",A).on("dblclick.zoom",M).filter(s).on("touchstart.zoom",$).on("touchmove.zoom",z).on("touchend.zoom touchcancel.zoom",E).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function y(t,e){return(e=Math.max(h[0],Math.min(h[1],e)))===t.k?t:new jn(e,t.x,t.y)}function v(t,e,n){var r=e[0]-n[0]*t.k,i=e[1]-n[1]*t.k;return r===t.x&&i===t.y?t:new jn(t.k,r,i)}function x(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function w(t,e,n,r){t.on("start.zoom",(function(){b(this,arguments).event(r).start()})).on("interrupt.zoom end.zoom",(function(){b(this,arguments).event(r).end()})).tween("zoom",(function(){var t=this,o=arguments,a=b(t,o).event(r),s=i.apply(t,o),h=null==n?x(s):"function"==typeof n?n.apply(t,o):n,u=Math.max(s[1][0]-s[0][0],s[1][1]-s[0][1]),l=t.__zoom,f="function"==typeof e?e.apply(t,o):e,p=c(l.invert(h).concat(u/l.k),f.invert(h).concat(u/f.k));return function(t){if(1===t)t=f;else{var e=p(t),n=u/e[2];t=new jn(n,h[0]-e[0]*n,h[1]-e[1]*n)}a.zoom(null,t)}}))}function b(t,e,n){return!n&&t.__zooming||new k(t,e)}function k(t,e){this.that=t,this.args=e,this.active=0,this.sourceEvent=null,this.extent=i.apply(t,e),this.taps=0}function N(t,...e){if(r.apply(this,arguments)){var n=b(this,e).event(t),i=this.__zoom,s=Math.max(h[0],Math.min(h[1],i.k*Math.pow(2,a.apply(this,arguments)))),l=It(t);if(n.wheel)n.mouse[0][0]===l[0]&&n.mouse[0][1]===l[1]||(n.mouse[1]=i.invert(n.mouse[0]=l)),clearTimeout(n.wheel);else{if(i.k===s)return;n.mouse=[l,i.invert(l)],xe(this),n.start()}Qn(t),n.wheel=setTimeout((function(){n.wheel=null,n.end()}),d),n.zoom("mouse",o(v(y(i,s),n.mouse[0],n.mouse[1]),n.extent,u))}}function A(t,...e){if(!n&&r.apply(this,arguments)){var i=t.currentTarget,a=b(this,e,!0).event(t),s=Rt(t.view).on("mousemove.zoom",(function(t){if(Qn(t),!a.moved){var e=t.clientX-l,n=t.clientY-c;a.moved=e*e+n*n>g}a.event(t).zoom("mouse",o(v(a.that.__zoom,a.mouse[0]=It(t,i),a.mouse[1]),a.extent,u))}),!0).on("mouseup.zoom",(function(t){s.on("mousemove.zoom mouseup.zoom",null),function(t,e){var n=t.document.documentElement,r=Rt(t).on("dragstart.drag",null);e&&(r.on("click.drag",On,Yn),setTimeout((function(){r.on("click.drag",null)}),0)),"onselectstart"in n?r.on("selectstart.drag",null):(n.style.MozUserSelect=n.__noselect,delete n.__noselect)}(t.view,a.moved),Qn(t),a.event(t).end()}),!0),h=It(t,i),l=t.clientX,c=t.clientY;!function(t){var e=t.document.documentElement,n=Rt(t).on("dragstart.drag",On,Yn);"onselectstart"in e?n.on("selectstart.drag",On,Yn):(e.__noselect=e.style.MozUserSelect,e.style.MozUserSelect="none")}(t.view),Kn(t),a.mouse=[h,this.__zoom.invert(h)],xe(this),a.start()}}function M(t,...e){if(r.apply(this,arguments)){var n=this.__zoom,a=It(t.changedTouches?t.changedTouches[0]:t,this),s=n.invert(a),h=n.k*(t.shiftKey?.5:2),c=o(v(y(n,h),a,s),i.apply(this,e),u);Qn(t),l>0?Rt(this).transition().duration(l).call(w,c,a,t):Rt(this).call(m.transform,c,a,t)}}function $(n,...i){if(r.apply(this,arguments)){var o,a,s,h,u=n.touches,l=u.length,c=b(this,i,n.changedTouches.length===l).event(n);for(Kn(n),a=0;a1+(t?Math.max(...t.map(e)):0),n=e(t);let i=r(t,(t=>this._configuration.showEmptyBoxes?(!t.parents&&t.generation.5));this._root=i,this._nodes=o(i)}get nodes(){return this._nodes}get root(){return this._root}createEmptyNode(t,e){return{id:0,xref:"",url:"",updateUrl:"",generation:t,name:"",firstNames:[],lastNames:[],preferredName:"",alternativeNames:[],isAltRtl:!1,sex:e,timespan:""}}}class mr{constructor(t,e=null){this._orientation=t,this._image=e,this._textPadding=15,this._x=this.calculateX(),this._y=this.calculateY(),this._width=this.calculateWidth()}calculateX(){const t=-this._orientation.boxWidth/2+this._textPadding;return this._image?t+this._image.width:t}calculateY(){return this._orientation instanceof fr||this._orientation instanceof pr?-this._textPadding:this._image?this._image.y+this._image.height+2*this._textPadding:-this._orientation.boxHeight/2+2*this._textPadding}calculateWidth(){const t=this._orientation.boxWidth-2*this._textPadding;return this._image&&(this._orientation instanceof fr||this._orientation instanceof pr)?t-this._image.width:t}get x(){return this._x}get y(){return this._y}get width(){return this._width}}class yr{constructor(t){this._cornerRadius=20,this._showImage=!0,this._orientation=t,this._x=-t.boxWidth/2,this._y=-t.boxHeight/2,this._rx=this._cornerRadius,this._ry=this._cornerRadius,this._width=t.boxWidth,this._height=t.boxHeight,this._image=new class{constructor(t,e){this._orientation=t,this._cornerRadius=e,this._imagePadding=5,this._imageRadius=Math.min(40,t.boxHeight/2-this._imagePadding),this._x=this.calculateX(),this._y=this.calculateY(),this._width=this.calculateImageWidth(),this._height=this.calculateImageHeight(),this._rx=this.calculateCornerRadius(),this._ry=this.calculateCornerRadius()}calculateX(){return-this._orientation.boxWidth/2+this._imagePadding}calculateY(){return this._orientation instanceof fr||this._orientation instanceof pr?-this._imageRadius:-this._orientation.boxHeight/2+this._imagePadding}calculateImageWidth(){return this._orientation instanceof fr||this._orientation instanceof pr?2*this._imageRadius:this._orientation.boxWidth-2*this._imagePadding}calculateImageHeight(){return 2*this._imageRadius}calculateCornerRadius(){return this._cornerRadius-this._imagePadding}get imagePadding(){return this._imagePadding}get imageRadius(){return this._imageRadius}set imageRadius(t){this._imageRadius=t}get x(){return this._x}get y(){return this._y}get rx(){return this._rx}get ry(){return this._ry}get width(){return this._width}get height(){return this._height}}(t,this._cornerRadius)}get showImage(){return this._showImage}set showImage(t){this._showImage=t}get x(){return this._x}get y(){return this._y}get rx(){return this._rx}get ry(){return this._ry}get width(){return this._width}get height(){return this._height}get image(){return this._image}get text(){return new mr(this._orientation,this._showImage?this._image:null)}}class vr{constructor(t,e,n){this._svg=t,this._configuration=e,this._hierarchy=n,this._hierarchy.root.x0=0,this._hierarchy.root.y0=0,this._orientation=this._configuration.orientation,this._box=new yr(this._orientation),this.draw(this._hierarchy.root)}draw(t){let e=this._hierarchy.nodes.descendants(),n=this._hierarchy.nodes.links();e.forEach((t=>{this._orientation.norm(t)})),this.drawLinks(n,t),this.drawNodes(e,t),e.forEach((t=>{t.x0=t.x,t.y0=t.y}))}drawNodes(t,e){let n=0,r=this;this._svg.defs.get().append("clipPath").attr("id","clip-image").append("rect").attr("rx",this._box.image.rx).attr("ry",this._box.image.ry).attr("x",this._box.image.x).attr("y",this._box.image.y).attr("width",this._box.image.width).attr("height",this._box.image.height);let i=this._svg.visual.selectAll("g.person").data(t,(t=>t.id||(t.id=++n))).enter().append("g").attr("class","person").attr("transform",(t=>"translate("+t.x+","+t.y+")"));i.append("rect").attr("class",(t=>"F"===t.data.sex?"female":t.data.sex===ar?"male":"unknown")).attr("rx",this._box.rx).attr("ry",this._box.ry).attr("x",this._box.x).attr("y",this._box.y).attr("width",this._box.width).attr("height",this._box.height).attr("fill-opacity",.5),i.filter((t=>""!==t.data.xref)).each((function(t){let e=Rt(this);e.append("title").text((t=>t.data.name));const n=r.getImageToLoad(t);if(r._box.showImage=!!n,r._box.showImage){let t=e.append("g").attr("class","image");t.append("rect").attr("rx",r._box.image.rx).attr("ry",r._box.image.ry).attr("x",r._box.image.x).attr("y",r._box.image.y).attr("width",r._box.image.width).attr("height",r._box.image.height).attr("fill","rgb(255, 255, 255)");let i=t.append("image").attr("x",r._box.image.x).attr("y",r._box.image.y).attr("width",r._box.image.width).attr("height",r._box.image.height).attr("clip-path","url(#clip-image)");(function(t,e=null){return fetch(t,e).then((t=>t.blob())).then((t=>new Promise(((e,n)=>{const r=new FileReader;r.onloadend=()=>e(r.result),r.onerror=n,r.readAsDataURL(t)}))))})(n).then((t=>i.attr("xlink:href",t))).catch((t=>{console.error(t)})),t.append("rect").attr("rx",r._box.image.rx).attr("ry",r._box.image.ry).attr("x",r._box.image.x).attr("y",r._box.image.y).attr("width",r._box.image.width).attr("height",r._box.image.height).attr("fill","none").attr("stroke","rgb(200, 200, 200)").attr("stroke-width",1.5)}r.addNames(e,t),r.addDates(e,t),r._box.showImage=!0}))}togglePerson(t,e){e.parents?(e._parents=e.parents,e.parents=null):(e.parents=e._parents,e._parents=null),this.draw(e)}collapse(t){t.parents&&(t._parents=t.parents,t._parents.forEach((t=>this.collapse(t))),t.parents=null)}addFirstNames(t,e){let n=0;for(let r of e.data.firstNames){let i=t.append("tspan").text(r);r===e.data.preferredName&&i.attr("class","preferred"),0!==n&&i.attr("dx","0.25em"),++n}}addLastNames(t,e,n=0){let r=0;for(let i of e.data.lastNames){let e=t.append("tspan").attr("class","lastName").text(i);0!==r&&e.attr("dx","0.25em"),0!==n&&e.attr("dx",n+"em"),++r}}truncateNames(t){let e=this._box.text.width;t.selectAll("tspan:not(.preferred):not(.lastName)").nodes().reverse().forEach((n=>Rt(n).each(this.truncateText(t,e)))),t.selectAll("tspan.preferred").each(this.truncateText(t,e)),t.selectAll("tspan.lastName").each(this.truncateText(t,e))}truncateText(t,e){let n=this;return function(){let r=n.getTextLength(t),i=Rt(this),o=i.text().split(/\s+/);for(let a=o.length-1;a>=0;--a)r>e&&(o[a]=o[a].slice(0,1)+".",i.text(o.join(" ")),r=n.getTextLength(t))}}truncateDate(t,e){let n=this;return function(){let r=n.getTextLength(t),i=Rt(this),o=i.text();for(;r>e&&o.length>1;)o=o.slice(0,-1).trim(),i.text(o),r=n.getTextLength(t);"."===o[o.length-1]&&i.text(o.slice(0,-1).trim())}}getTextLength(t){let e=0;return t.selectAll("tspan").each((function(){e+=this.getComputedTextLength()})),e}addNames(t,e){let n=t.append("g").attr("class","name");if(this._orientation.splittNames){let t=n.append("text").attr("class","wt-chart-box-name").attr("text-anchor","middle").attr("alignment-baseline","central").attr("dy",this._box.text.y),r=n.append("text").attr("class","wt-chart-box-name").attr("text-anchor","middle").attr("alignment-baseline","central").attr("dy",this._box.text.y+20);this.addFirstNames(t,e),this.addLastNames(r,e),e.data.firstNames.length||e.data.lastNames.length||t.append("tspan").text(e.data.name),this.truncateNames(t),this.truncateNames(r)}else{let t=n.append("text").attr("class","wt-chart-box-name").attr("text-anchor",this._configuration.rtl?"end":"start").attr("dx",this._box.text.x).attr("dy",this._box.text.y);this.addFirstNames(t,e),this.addLastNames(t,e,.25),e.data.firstNames.length||e.data.lastNames.length||t.append("tspan").text(e.data.name),this.truncateNames(t)}}addDates(t,e){let n=t.append("g").attr("class","table");if(this._orientation.splittNames){let t=n.append("text").attr("class","date").attr("text-anchor","middle").attr("alignment-baseline","central").attr("dy",this._box.text.y+50);t.append("title").text(e.data.timespan);let r=t.append("tspan").text(e.data.timespan);return void(this.getTextLength(t)>this._box.text.width&&(t.selectAll("tspan").each(this.truncateDate(t,this._box.text.width)),r.text(r.text()+"…")))}let r=20;if(e.data.birth){n.append("text").attr("fill","currentColor").attr("text-anchor","middle").attr("dominant-baseline","middle").attr("x",this._box.text.x).attr("dy",this._box.text.y+r).append("tspan").text("★").attr("x",this._box.text.x+5);let t=n.append("text").attr("class","date").attr("text-anchor",this._configuration.rtl?"end":"start").attr("dominant-baseline","middle").attr("x",this._box.text.x).attr("dy",this._box.text.y+r);t.append("title").text(e.data.birth);let i=t.append("tspan").text(e.data.birth).attr("x",this._box.text.x+15);this.getTextLength(t)>this._box.text.width-25&&(t.selectAll("tspan").each(this.truncateDate(t,this._box.text.width-25)),i.text(i.text()+"…"))}if(e.data.death){e.data.birth&&(r+=20),n.append("text").attr("fill","currentColor").attr("text-anchor","middle").attr("dominant-baseline","middle").attr("x",this._box.text.x).attr("dy",this._box.text.y+r).append("tspan").text("†").attr("x",this._box.text.x+5);let t=n.append("text").attr("class","date").attr("text-anchor",this._configuration.rtl?"end":"start").attr("dominant-baseline","middle").attr("x",this._box.text.x).attr("dy",this._box.text.y+r);t.append("title").text(e.data.death);let i=t.append("tspan").text(e.data.death).attr("x",this._box.text.x+15);this.getTextLength(t)>this._box.text.width-25&&(t.selectAll("tspan").each(this.truncateDate(t,this._box.text.width-25)),i.text(i.text().trim()+"…"))}}getImageToLoad(t){return t.data.thumbnail?t.data.thumbnail:""}drawLinks(t,e){this._svg.visual.selectAll("path.link").data(t).enter().append("path").classed("link",!0).attr("d",(t=>this._orientation.elbow(t)))}}class xr{constructor(t){this._element=t.append("div").attr("class","overlay").style("opacity",1e-6)}show(t,e=0,n=null){this._element.select("p").remove(),this._element.append("p").attr("class","tooltip").text(t),this._element.transition().duration(e).style("opacity",1).on("end",(()=>{"function"==typeof n&&n()}))}hide(t=0,e=0){this._element.transition().delay(t).duration(e).style("opacity",1e-6)}get(){return this._element}}class wr{constructor(t){this._element=t.append("defs")}get(){return this._element}}class br{constructor(t){this._zoom=null,this._parent=t,this.init()}init(){this._zoom=ir(),this._zoom.scaleExtent([.1,20]).on("zoom",(t=>{this._parent.attr("transform",t.transform)})),this._zoom.wheelDelta((t=>-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002))),this._zoom.filter((t=>{if("wheel"===t.type){if(!t.ctrlKey)return!1;var e=Gn(this);if(e.k){if(e.k<=.1&&t.deltaY>0)return t.preventDefault(),!1;if(e.k>=20&&t.deltaY<0)return t.preventDefault(),!1}return!0}return t.button||"touchstart"!==t.type?!(t.ctrlKey&&"wheel"!==t.type||t.button):2===t.touches.length}))}get(){return this._zoom}}class kr{triggerDownload(t,e){let n=new MouseEvent("click",{view:window,bubbles:!1,cancelable:!0}),r=document.createElement("a");r.setAttribute("download",e),r.setAttribute("href",t),r.setAttribute("target","_blank"),r.dispatchEvent(n)}}class Nr extends kr{copyStylesInline(t,e){let n=["svg","g","text","textPath"];for(let r=0;r{let i=(new XMLSerializer).serializeToString(t),o=window.URL||window.webkitURL||window,a=new Blob([i],{type:"image/svg+xml;charset=utf-8"}),s=o.createObjectURL(a),h=new Image;h.onload=()=>{let t=this.createCanvas(e,n),i=t.getContext("2d");i.fillStyle="rgb(255,255,255)",i.fillRect(0,0,t.width,t.height),i.drawImage(h,0,0),o.revokeObjectURL(s);let a=t.toDataURL("image/png").replace("image/png","image/octet-stream");r(a)},h.src=s}))}cloneSvg(t){return new Promise((e=>{e(t.cloneNode(!0))}))}svgToImage(t,e){const n=[4960,3508];this.cloneSvg(t.get().node()).then((e=>{this.copyStylesInline(t.get().node(),e);const r=this.calculateViewBox(t.get().node()),i=Math.max(n[0],r[2]),o=Math.max(n[1],r[3]);return e.setAttribute("width",""+i),e.setAttribute("height",""+o),e.setAttribute("viewBox",""+r),this.convertToDataUrl(e,i,o)})).then((t=>this.triggerDownload(t,e))).catch((()=>{console.log("Failed to save chart as PNG image")}))}}class Ar extends kr{copyStylesInline(t,n){return new Promise((r=>{Promise.all(t.map((t=>function(t,n){return fetch(t,n).then(e)}(t)))).then((t=>{t.forEach((t=>{t=t.replace(/.webtrees-pedigree-chart-container /g,"");let e=document.createElementNS("http://www.w3.org/2000/svg","style");e.appendChild(document.createTextNode(t)),n.prepend(e)})),n.classList.add("wt-global"),r(n)}))}))}convertToObjectUrl(t){return new Promise((e=>{let n=(new XMLSerializer).serializeToString(t),r=window.URL||window.webkitURL||window,i=new Blob([n],{type:"image/svg+xml;charset=utf-8"}),o=r.createObjectURL(i),a=new Image;a.onload=()=>{e(o)},a.src=o}))}cloneSvg(t){return new Promise((e=>{e(t.cloneNode(!0))}))}svgToImage(t,e,n){this.cloneSvg(t.get().node()).then((t=>this.copyStylesInline(e,t))).then((t=>this.convertToObjectUrl(t))).then((t=>this.triggerDownload(t,n))).catch((()=>{console.log("Failed to save chart as SVG image")}))}}class Mr{constructor(){this._exportClass=null}setExportClass(t){switch(t){case"png":this._exportClass=Nr;break;case"svg":this._exportClass=Ar}}createExport(t){switch(this.setExportClass(t),t){case"png":case"svg":return new this._exportClass}}}class $r{constructor(t,e){this._element=t.append("svg"),this._defs=new wr(this._element),this._visual=null,this._zoom=null,this._configuration=e,this.init()}init(){this._element.attr("width","100%").attr("height","100%").attr("text-rendering","optimizeLegibility").attr("text-anchor","middle").attr("xmlns:xlink","https://www.w3.org/1999/xlink")}initEvents(t){this._element.on("contextmenu",(t=>t.preventDefault())).on("wheel",(e=>{e.ctrlKey||t.show(this._configuration.labels.zoom,300,(()=>{t.hide(700,800)}))})).on("touchend",(e=>{e.touches.length<2&&t.hide(0,800)})).on("touchmove",(e=>{e.touches.length>=2?t.hide():t.show(this._configuration.labels.move)})).on("click",(t=>this.doStopPropagation(t)),!0),this._configuration.rtl&&this._element.classed("rtl",!0),this._visual=this._element.append("g"),this._zoom=new br(this._visual),this._element.call(this._zoom.get())}doStopPropagation(t){t.defaultPrevented&&t.stopPropagation()}export(t){return(new Mr).createExport(t)}get defs(){return this._defs}get zoom(){return this._zoom}get visual(){return this._visual}get(){return this._element}}class zr{constructor(t,e){this._configuration=e,this._parent=t,this._hierarchy=new _r(this._configuration),this._data={}}get svg(){return this._svg}updateViewBox(){let t=this._svg.visual.node().getBBox(),e=this._parent.node().getBoundingClientRect(),n=Math.max(e.width,t.width),r=Math.max(e.height,t.height,300),i=(n-t.width)/2,o=(r-t.height)/2,a=Math.ceil(t.x-i-10),s=Math.ceil(t.y-o-10);n=Math.ceil(n+20),r=Math.ceil(r+20),this._svg.get().attr("viewBox",[a,s,n,r])}get data(){return this._data}set data(t){this._data=t,this._hierarchy.init(this._data)}draw(){this._parent.html(""),this._svg=new $r(this._parent,this._configuration),this._overlay=new xr(this._parent),this._svg.initEvents(this._overlay),new vr(this._svg,this._configuration,this._hierarchy),this.bindClickEventListener(),this.updateViewBox()}bindClickEventListener(){let t=this;this._svg.visual.selectAll("g.person").filter((t=>""!==t.data.xref)).each((function(e){Rt(this).on("click",(function(){t.personClick(e.data)}))}))}personClick(t){1===t.generation?this.redirectToIndividual(t.url):this.update(t.updateUrl)}redirectToIndividual(t){window.open(t,"_blank")}update(t){window.location=t}}t.PedigreeChart=class{constructor(t,e){this._selector=t,this._parent=Rt(this._selector),this._configuration=new gr(e.labels,e.generations,e.showEmptyBoxes,e.treeLayout,e.rtl),this._cssFiles=e.cssFiles,this._chart=new zr(this._parent,this._configuration),this.init(),this.draw(e.data)}init(){Rt("#centerButton").on("click",(()=>this.center())),Rt("#exportPNG").on("click",(()=>this.exportPNG())),Rt("#exportSVG").on("click",(()=>this.exportSVG()))}center(){this._chart.svg.get().transition().duration(750).call(this._chart.svg.zoom.get().transform,Fn)}get configuration(){return this._configuration}update(t){this._chart.update(t)}draw(t){this._chart.data=t,this._chart.draw()}exportPNG(){this._chart.svg.export("png").svgToImage(this._chart.svg,"pedigree-chart.png")}exportSVG(){this._chart.svg.export("svg").svgToImage(this._chart.svg,this._cssFiles,"pedigree-chart.svg")}}},"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).WebtreesPedigreeChart={}); +var t,e;t=this,e=function(t){function e(t){if(!t.ok)throw new Error(t.status+" "+t.statusText);return t.text()}function n(t){var e=0,n=t.children,i=n&&n.length;if(i)for(;--i>=0;)e+=n[i].value;else e=1;t.value=e}function i(t,e){t instanceof Map?(t=[void 0,t],void 0===e&&(e=a)):void 0===e&&(e=r);for(var n,i,o,l,u,c=new h(t),f=[c];n=f.pop();)if((o=e(n.data))&&(u=(o=Array.from(o)).length))for(n.children=o,l=u-1;l>=0;--l)f.push(i=o[l]=new h(o[l])),i.parent=n,i.depth=n.depth+1;return c.eachBefore(s)}function r(t){return t.children}function a(t){return Array.isArray(t)?t[1]:null}function o(t){void 0!==t.data.value&&(t.value=t.data.value),t.data=t.data.data}function s(t){var e=0;do{t.height=e}while((t=t.parent)&&t.height<++e)}function h(t){this.data=t,this.depth=this.height=0,this.parent=null}function l(t,e){return t.parent===e.parent?1:2}function u(t){var e=t.children;return e?e[0]:t.t}function c(t){var e=t.children;return e?e[e.length-1]:t.t}function f(t,e,n){var i=n/(e.i-t.i);e.c-=i,e.s+=n,t.c+=i,e.z+=n,e.m+=n}function d(t,e,n){return t.a.parent===e.parent?t.a:n}function p(t,e){this._=t,this.parent=null,this.children=null,this.A=null,this.a=this,this.z=0,this.m=0,this.c=0,this.s=0,this.t=null,this.i=e}function g(){var t=l,e=1,n=1,i=null;function r(r){var h=function(t){for(var e,n,i,r,a,o=new p(t,0),s=[o];e=s.pop();)if(i=e._.children)for(e.children=new Array(a=i.length),r=a-1;r>=0;--r)s.push(n=e.children[r]=new p(i[r],r)),n.parent=e;return(o.parent=new p(null,0)).children=[o],o}(r);if(h.eachAfter(a),h.parent.m=-h.z,h.eachBefore(o),i)r.eachBefore(s);else{var l=r,u=r,c=r;r.eachBefore((function(t){t.xu.x&&(u=t),t.depth>c.depth&&(c=t)}));var f=l===u?1:t(l,u)/2,d=f-l.x,g=e/(u.x+f+d),_=n/(c.depth||1);r.eachBefore((function(t){t.x=(t.x+d)*g,t.y=t.depth*_}))}return r}function a(e){var n=e.children,i=e.parent.children,r=e.i?i[e.i-1]:null;if(n){!function(t){for(var e,n=0,i=0,r=t.children,a=r.length;--a>=0;)(e=r[a]).z+=n,e.m+=n,n+=e.s+(i+=e.c)}(e);var a=(n[0].z+n[n.length-1].z)/2;r?(e.z=r.z+t(e._,r._),e.m=e.z-a):e.z=a}else r&&(e.z=r.z+t(e._,r._));e.parent.A=function(e,n,i){if(n){for(var r,a=e,o=e,s=n,h=a.parent.children[0],l=a.m,p=o.m,g=s.m,_=h.m;s=c(s),a=u(a),s&&a;)h=u(h),(o=c(o)).a=e,(r=s.z+g-a.z-l+t(s._,a._))>0&&(f(d(s,e,i),e,r),l+=r,p+=r),g+=s.m,l+=a.m,_+=h.m,p+=o.m;s&&!c(o)&&(o.t=s,o.m+=g-p),a&&!u(h)&&(h.t=a,h.m+=l-_,i=e)}return i}(e,r,e.parent.A||i[0])}function o(t){t._.x=t.z+t.parent.m,t.m+=t.parent.m}function s(t){t.x*=e,t.y=t.depth*n}return r.separation=function(e){return arguments.length?(t=e,r):t},r.size=function(t){return arguments.length?(i=!1,e=+t[0],n=+t[1],r):i?null:[e,n]},r.nodeSize=function(t){return arguments.length?(i=!0,e=+t[0],n=+t[1],r):i?[e,n]:null},r}h.prototype=i.prototype={constructor:h,count:function(){return this.eachAfter(n)},each:function(t,e){let n=-1;for(const i of this)t.call(e,i,++n,this);return this},eachAfter:function(t,e){for(var n,i,r,a=this,o=[a],s=[],h=-1;a=o.pop();)if(s.push(a),n=a.children)for(i=0,r=n.length;i=0;--i)a.push(n[i]);return this},find:function(t,e){let n=-1;for(const i of this)if(t.call(e,i,++n,this))return i},sum:function(t){return this.eachAfter((function(e){for(var n=+t(e.data)||0,i=e.children,r=i&&i.length;--r>=0;)n+=i[r].value;e.value=n}))},sort:function(t){return this.eachBefore((function(e){e.children&&e.children.sort(t)}))},path:function(t){for(var e=this,n=function(t,e){if(t===e)return t;var n=t.ancestors(),i=e.ancestors(),r=null;for(t=n.pop(),e=i.pop();t===e;)r=t,t=n.pop(),e=i.pop();return r}(e,t),i=[e];e!==n;)e=e.parent,i.push(e);for(var r=i.length;t!==n;)i.splice(r,0,t),t=t.parent;return i},ancestors:function(){for(var t=this,e=[t];t=t.parent;)e.push(t);return e},descendants:function(){return Array.from(this)},leaves:function(){var t=[];return this.eachBefore((function(e){e.children||t.push(e)})),t},links:function(){var t=this,e=[];return t.each((function(n){n!==t&&e.push({source:n.parent,target:n})})),e},copy:function(){return i(this).eachBefore(o)},[Symbol.iterator]:function*(){var t,e,n,i,r=this,a=[r];do{for(t=a.reverse(),a=[];r=t.pop();)if(yield r,e=r.children)for(n=0,i=e.length;n=0))throw new Error(`invalid digits: ${t}`);if(e>15)return w;const n=10**e;return function(t){this._+=t[0];for(let e=1,i=t.length;ey)if(Math.abs(u*s-h*l)>y&&r){let f=n-a,d=i-o,p=s*s+h*h,g=f*f+d*d,m=Math.sqrt(p),v=Math.sqrt(c),w=r*Math.tan((_-Math.acos((p+c-g)/(2*m*v)))/2),x=w/v,b=w/m;Math.abs(x-1)>y&&this._append`L${t+x*l},${e+x*u}`,this._append`A${r},${r},0,0,${+(u*f>l*d)},${this._x1=t+b*s},${this._y1=e+b*h}`}else this._append`L${this._x1=t},${this._y1=e}`}arc(t,e,n,i,r,a){if(t=+t,e=+e,a=!!a,(n=+n)<0)throw new Error(`negative radius: ${n}`);let o=n*Math.cos(i),s=n*Math.sin(i),h=t+o,l=e+s,u=1^a,c=a?i-r:r-i;null===this._x1?this._append`M${h},${l}`:(Math.abs(this._x1-h)>y||Math.abs(this._y1-l)>y)&&this._append`L${h},${l}`,n&&(c<0&&(c=c%m+m),c>v?this._append`A${n},${n},0,1,${u},${t-o},${e-s}A${n},${n},0,1,${u},${this._x1=h},${this._y1=l}`:c>y&&this._append`A${n},${n},0,${+(c>=_)},${u},${this._x1=t+n*Math.cos(r)},${this._y1=e+n*Math.sin(r)}`)}rect(t,e,n,i){this._append`M${this._x0=this._x1=+t},${this._y0=this._y1=+e}h${n=+n}v${+i}h${-n}Z`}toString(){return this._}}function b(){return new x}b.prototype=x.prototype;var N="http://www.w3.org/1999/xhtml",k={svg:"http://www.w3.org/2000/svg",xhtml:N,xlink:"http://www.w3.org/1999/xlink",xml:"http://www.w3.org/XML/1998/namespace",xmlns:"http://www.w3.org/2000/xmlns/"};function A(t){var e=t+="",n=e.indexOf(":");return n>=0&&"xmlns"!==(e=t.slice(0,n))&&(t=t.slice(n+1)),k.hasOwnProperty(e)?{space:k[e],local:t}:t}function E(t){return function(){var e=this.ownerDocument,n=this.namespaceURI;return n===N&&e.documentElement.namespaceURI===N?e.createElement(t):e.createElementNS(n,t)}}function M(t){return function(){return this.ownerDocument.createElementNS(t.space,t.local)}}function $(t){var e=A(t);return(e.local?M:E)(e)}function z(){}function T(t){return null==t?z:function(){return this.querySelector(t)}}function R(t){return null==t?[]:Array.isArray(t)?t:Array.from(t)}function S(){return[]}function P(t){return null==t?S:function(){return this.querySelectorAll(t)}}function C(t){return function(){return this.matches(t)}}function D(t){return function(e){return e.matches(t)}}var L=Array.prototype.find;function O(){return this.firstElementChild}var B=Array.prototype.filter;function I(){return Array.from(this.children)}function X(t){return new Array(t.length)}function H(t,e){this.ownerDocument=t.ownerDocument,this.namespaceURI=t.namespaceURI,this._next=null,this._parent=t,this.__data__=e}function W(t,e,n,i,r,a){for(var o,s=0,h=e.length,l=a.length;se?1:t>=e?0:NaN}function V(t){return function(){this.removeAttribute(t)}}function G(t){return function(){this.removeAttributeNS(t.space,t.local)}}function F(t,e){return function(){this.setAttribute(t,e)}}function K(t,e){return function(){this.setAttributeNS(t.space,t.local,e)}}function Q(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttribute(t):this.setAttribute(t,n)}}function Z(t,e){return function(){var n=e.apply(this,arguments);null==n?this.removeAttributeNS(t.space,t.local):this.setAttributeNS(t.space,t.local,n)}}function J(t){return t.ownerDocument&&t.ownerDocument.defaultView||t.document&&t||t.defaultView}function tt(t){return function(){this.style.removeProperty(t)}}function et(t,e,n){return function(){this.style.setProperty(t,e,n)}}function nt(t,e,n){return function(){var i=e.apply(this,arguments);null==i?this.style.removeProperty(t):this.style.setProperty(t,i,n)}}function it(t,e){return t.style.getPropertyValue(e)||J(t).getComputedStyle(t,null).getPropertyValue(e)}function rt(t){return function(){delete this[t]}}function at(t,e){return function(){this[t]=e}}function ot(t,e){return function(){var n=e.apply(this,arguments);null==n?delete this[t]:this[t]=n}}function st(t){return t.trim().split(/^|\s+/)}function ht(t){return t.classList||new lt(t)}function lt(t){this._node=t,this._names=st(t.getAttribute("class")||"")}function ut(t,e){for(var n=ht(t),i=-1,r=e.length;++i=0&&(this._names.splice(e,1),this._node.setAttribute("class",this._names.join(" ")))},contains:function(t){return this._names.indexOf(t)>=0}};var St=[null];function Pt(t,e){this._groups=t,this._parents=e}function Ct(){return new Pt([[document.documentElement]],St)}function Dt(t){return"string"==typeof t?new Pt([[document.querySelector(t)]],[document.documentElement]):new Pt([[t]],St)}function Lt(t,e){if(t=function(t){let e;for(;e=t.sourceEvent;)t=e;return t}(t),void 0===e&&(e=t.currentTarget),e){var n=e.ownerSVGElement||e;if(n.createSVGPoint){var i=n.createSVGPoint();return i.x=t.clientX,i.y=t.clientY,[(i=i.matrixTransform(e.getScreenCTM().inverse())).x,i.y]}if(e.getBoundingClientRect){var r=e.getBoundingClientRect();return[t.clientX-r.left-e.clientLeft,t.clientY-r.top-e.clientTop]}}return[t.pageX,t.pageY]}Pt.prototype=Ct.prototype={constructor:Pt,select:function(t){"function"!=typeof t&&(t=T(t));for(var e=this._groups,n=e.length,i=new Array(n),r=0;r=x&&(x=w+1);!(v=m[x])&&++x=0;)(i=r[a])&&(o&&4^i.compareDocumentPosition(o)&&o.parentNode.insertBefore(i,o),o=i);return this},sort:function(t){function e(e,n){return e&&n?t(e.__data__,n.__data__):!e-!n}t||(t=j);for(var n=this._groups,i=n.length,r=new Array(i),a=0;a1?this.each((null==e?tt:"function"==typeof e?nt:et)(t,e,null==n?"":n)):it(this.node(),t)},property:function(t,e){return arguments.length>1?this.each((null==e?rt:"function"==typeof e?ot:at)(t,e)):this.node()[t]},classed:function(t,e){var n=st(t+"");if(arguments.length<2){for(var i=ht(this.node()),r=-1,a=n.length;++r=0&&(e=t.slice(n+1),t=t.slice(0,n)),{type:t,name:e}}))}(t+""),o=a.length;if(!(arguments.length<2)){for(s=e?$t:Mt,i=0;i{}};function Bt(){for(var t,e=0,n=arguments.length,i={};e=0&&(e=t.slice(n+1),t=t.slice(0,n)),t&&!i.hasOwnProperty(t))throw new Error("unknown type: "+t);return{type:t,name:e}}))),o=-1,s=a.length;if(!(arguments.length<2)){if(null!=e&&"function"!=typeof e)throw new Error("invalid callback: "+e);for(;++o0)for(var n,i,r=new Array(n),a=0;a=0&&e._call.call(void 0,t),e=e._next;--qt}()}finally{qt=0,function(){for(var t,e,n=Wt,i=1/0;n;)n._call?(i>n._time&&(i=n._time),t=n,n=n._next):(e=n._next,n._next=null,n=t?t._next=e:Wt=e);Yt=t,ae(i)}(),Ft=0}}function re(){var t=Qt.now(),e=t-Gt;e>Vt&&(Kt-=e,Gt=t)}function ae(t){qt||(Ut&&(Ut=clearTimeout(Ut)),t-Ft>24?(t<1/0&&(Ut=setTimeout(ie,t-Qt.now()-Kt)),jt&&(jt=clearInterval(jt))):(jt||(Gt=Qt.now(),jt=setInterval(re,Vt)),qt=1,Zt(ie)))}function oe(t,e,n){var i=new ee;return e=null==e?0:+e,i.restart((n=>{i.stop(),t(n+e)}),e,n),i}ee.prototype=ne.prototype={constructor:ee,restart:function(t,e,n){if("function"!=typeof t)throw new TypeError("callback is not a function");n=(null==n?Jt():+n)+(null==e?0:+e),this._next||Yt===this||(Yt?Yt._next=this:Wt=this,Yt=this),this._call=t,this._time=n,ae()},stop:function(){this._call&&(this._call=null,this._time=1/0,ae())}};var se=Bt("start","end","cancel","interrupt"),he=[],le=0,ue=1,ce=2,fe=3,de=4,pe=5,ge=6;function _e(t,e,n,i,r,a){var o=t.__transition;if(o){if(n in o)return}else t.__transition={};!function(t,e,n){var i,r=t.__transition;function a(t){n.state=ue,n.timer.restart(o,n.delay,n.time),n.delay<=t&&o(t-n.delay)}function o(a){var l,u,c,f;if(n.state!==ue)return h();for(l in r)if((f=r[l]).name===n.name){if(f.state===fe)return oe(o);f.state===de?(f.state=ge,f.timer.stop(),f.on.call("interrupt",t,t.__data__,f.index,f.group),delete r[l]):+lle)throw new Error("too late; already scheduled");return n}function ye(t,e){var n=ve(t,e);if(n.state>fe)throw new Error("too late; already running");return n}function ve(t,e){var n=t.__transition;if(!n||!(n=n[e]))throw new Error("transition not found");return n}function we(t,e){var n,i,r,a=t.__transition,o=!0;if(a){for(r in e=null==e?null:e+"",a)(n=a[r]).name===e?(i=n.state>ce&&n.state>8&15|e>>4&240,e>>4&15|240&e,(15&e)<<4|15&e,1):8===n?He(e>>24&255,e>>16&255,e>>8&255,(255&e)/255):4===n?He(e>>12&15|e>>8&240,e>>8&15|e>>4&240,e>>4&15|240&e,((15&e)<<4|15&e)/255):null):(e=Te.exec(t))?new Ye(e[1],e[2],e[3],1):(e=Re.exec(t))?new Ye(255*e[1]/100,255*e[2]/100,255*e[3]/100,1):(e=Se.exec(t))?He(e[1],e[2],e[3],e[4]):(e=Pe.exec(t))?He(255*e[1]/100,255*e[2]/100,255*e[3]/100,e[4]):(e=Ce.exec(t))?Fe(e[1],e[2]/100,e[3]/100,1):(e=De.exec(t))?Fe(e[1],e[2]/100,e[3]/100,e[4]):Le.hasOwnProperty(t)?Xe(Le[t]):"transparent"===t?new Ye(NaN,NaN,NaN,0):null}function Xe(t){return new Ye(t>>16&255,t>>8&255,255&t,1)}function He(t,e,n,i){return i<=0&&(t=e=n=NaN),new Ye(t,e,n,i)}function We(t,e,n,i){return 1===arguments.length?((r=t)instanceof Ne||(r=Ie(r)),r?new Ye((r=r.rgb()).r,r.g,r.b,r.opacity):new Ye):new Ye(t,e,n,null==i?1:i);var r}function Ye(t,e,n,i){this.r=+t,this.g=+e,this.b=+n,this.opacity=+i}function qe(){return`#${Ge(this.r)}${Ge(this.g)}${Ge(this.b)}`}function Ue(){const t=je(this.opacity);return`${1===t?"rgb(":"rgba("}${Ve(this.r)}, ${Ve(this.g)}, ${Ve(this.b)}${1===t?")":`, ${t})`}`}function je(t){return isNaN(t)?1:Math.max(0,Math.min(1,t))}function Ve(t){return Math.max(0,Math.min(255,Math.round(t)||0))}function Ge(t){return((t=Ve(t))<16?"0":"")+t.toString(16)}function Fe(t,e,n,i){return i<=0?t=e=n=NaN:n<=0||n>=1?t=e=NaN:e<=0&&(t=NaN),new Qe(t,e,n,i)}function Ke(t){if(t instanceof Qe)return new Qe(t.h,t.s,t.l,t.opacity);if(t instanceof Ne||(t=Ie(t)),!t)return new Qe;if(t instanceof Qe)return t;var e=(t=t.rgb()).r/255,n=t.g/255,i=t.b/255,r=Math.min(e,n,i),a=Math.max(e,n,i),o=NaN,s=a-r,h=(a+r)/2;return s?(o=e===a?(n-i)/s+6*(n0&&h<1?0:o,new Qe(o,s,h,t.opacity)}function Qe(t,e,n,i){this.h=+t,this.s=+e,this.l=+n,this.opacity=+i}function Ze(t){return(t=(t||0)%360)<0?t+360:t}function Je(t){return Math.max(0,Math.min(1,t||0))}function tn(t,e,n){return 255*(t<60?e+(n-e)*t/60:t<180?n:t<240?e+(n-e)*(240-t)/60:e)}xe(Ne,Ie,{copy(t){return Object.assign(new this.constructor,this,t)},displayable(){return this.rgb().displayable()},hex:Oe,formatHex:Oe,formatHex8:function(){return this.rgb().formatHex8()},formatHsl:function(){return Ke(this).formatHsl()},formatRgb:Be,toString:Be}),xe(Ye,We,be(Ne,{brighter(t){return t=null==t?Ae:Math.pow(Ae,t),new Ye(this.r*t,this.g*t,this.b*t,this.opacity)},darker(t){return t=null==t?ke:Math.pow(ke,t),new Ye(this.r*t,this.g*t,this.b*t,this.opacity)},rgb(){return this},clamp(){return new Ye(Ve(this.r),Ve(this.g),Ve(this.b),je(this.opacity))},displayable(){return-.5<=this.r&&this.r<255.5&&-.5<=this.g&&this.g<255.5&&-.5<=this.b&&this.b<255.5&&0<=this.opacity&&this.opacity<=1},hex:qe,formatHex:qe,formatHex8:function(){return`#${Ge(this.r)}${Ge(this.g)}${Ge(this.b)}${Ge(255*(isNaN(this.opacity)?1:this.opacity))}`},formatRgb:Ue,toString:Ue})),xe(Qe,(function(t,e,n,i){return 1===arguments.length?Ke(t):new Qe(t,e,n,null==i?1:i)}),be(Ne,{brighter(t){return t=null==t?Ae:Math.pow(Ae,t),new Qe(this.h,this.s,this.l*t,this.opacity)},darker(t){return t=null==t?ke:Math.pow(ke,t),new Qe(this.h,this.s,this.l*t,this.opacity)},rgb(){var t=this.h%360+360*(this.h<0),e=isNaN(t)||isNaN(this.s)?0:this.s,n=this.l,i=n+(n<.5?n:1-n)*e,r=2*n-i;return new Ye(tn(t>=240?t-240:t+120,r,i),tn(t,r,i),tn(t<120?t+240:t-120,r,i),this.opacity)},clamp(){return new Qe(Ze(this.h),Je(this.s),Je(this.l),je(this.opacity))},displayable(){return(0<=this.s&&this.s<=1||isNaN(this.s))&&0<=this.l&&this.l<=1&&0<=this.opacity&&this.opacity<=1},formatHsl(){const t=je(this.opacity);return`${1===t?"hsl(":"hsla("}${Ze(this.h)}, ${100*Je(this.s)}%, ${100*Je(this.l)}%${1===t?")":`, ${t})`}`}}));var en=t=>()=>t;function nn(t){return 1==(t=+t)?rn:function(e,n){return n-e?function(t,e,n){return t=Math.pow(t,n),e=Math.pow(e,n)-t,n=1/n,function(i){return Math.pow(t+i*e,n)}}(e,n,t):en(isNaN(e)?n:e)}}function rn(t,e){var n=e-t;return n?function(t,e){return function(n){return t+n*e}}(t,n):en(isNaN(t)?e:t)}var an=function t(e){var n=nn(e);function i(t,e){var i=n((t=We(t)).r,(e=We(e)).r),r=n(t.g,e.g),a=n(t.b,e.b),o=rn(t.opacity,e.opacity);return function(e){return t.r=i(e),t.g=r(e),t.b=a(e),t.opacity=o(e),t+""}}return i.gamma=t,i}(1);function on(t,e){return t=+t,e=+e,function(n){return t*(1-n)+e*n}}var sn=/[-+]?(?:\d+\.?\d*|\.?\d+)(?:[eE][-+]?\d+)?/g,hn=new RegExp(sn.source,"g");function ln(t,e){var n,i,r,a=sn.lastIndex=hn.lastIndex=0,o=-1,s=[],h=[];for(t+="",e+="";(n=sn.exec(t))&&(i=hn.exec(e));)(r=i.index)>a&&(r=e.slice(a,r),s[o]?s[o]+=r:s[++o]=r),(n=n[0])===(i=i[0])?s[o]?s[o]+=i:s[++o]=i:(s[++o]=null,h.push({i:o,x:on(n,i)})),a=hn.lastIndex;return a180?e+=360:e-t>180&&(t+=360),a.push({i:n.push(r(n)+"rotate(",null,i)-2,x:on(t,e)})):e&&n.push(r(n)+"rotate("+e+i)}(a.rotate,o.rotate,s,h),function(t,e,n,a){t!==e?a.push({i:n.push(r(n)+"skewX(",null,i)-2,x:on(t,e)}):e&&n.push(r(n)+"skewX("+e+i)}(a.skewX,o.skewX,s,h),function(t,e,n,i,a,o){if(t!==n||e!==i){var s=a.push(r(a)+"scale(",null,",",null,")");o.push({i:s-4,x:on(t,n)},{i:s-2,x:on(e,i)})}else 1===n&&1===i||a.push(r(a)+"scale("+n+","+i+")")}(a.scaleX,a.scaleY,o.scaleX,o.scaleY,s,h),a=o=null,function(t){for(var e,n=-1,i=h.length;++n=0&&(t=t.slice(0,e)),!t||"start"===t}))}(e)?me:ye;return function(){var o=a(this,t),s=o.on;s!==i&&(r=(i=s).copy()).on(e,n),o.on=r}}(n,t,e))},attr:function(t,e){var n=A(t),i="transform"===n?_n:bn;return this.attrTween(t,"function"==typeof e?(n.local?$n:Mn)(n,i,xn(this,"attr."+t,e)):null==e?(n.local?kn:Nn)(n):(n.local?En:An)(n,i,e))},attrTween:function(t,e){var n="attr."+t;if(arguments.length<2)return(n=this.tween(n))&&n._value;if(null==e)return this.tween(n,null);if("function"!=typeof e)throw new Error;var i=A(t);return this.tween(n,(i.local?zn:Tn)(i,e))},style:function(t,e,n){var i="transform"==(t+="")?gn:bn;return null==e?this.styleTween(t,function(t,e){var n,i,r;return function(){var a=it(this,t),o=(this.style.removeProperty(t),it(this,t));return a===o?null:a===n&&o===i?r:r=e(n=a,i=o)}}(t,i)).on("end.style."+t,Ln(t)):"function"==typeof e?this.styleTween(t,function(t,e,n){var i,r,a;return function(){var o=it(this,t),s=n(this),h=s+"";return null==s&&(this.style.removeProperty(t),h=s=it(this,t)),o===h?null:o===i&&h===r?a:(r=h,a=e(i=o,s))}}(t,i,xn(this,"style."+t,e))).each(function(t,e){var n,i,r,a,o="style."+e,s="end."+o;return function(){var h=ye(this,t),l=h.on,u=null==h.value[o]?a||(a=Ln(e)):void 0;l===n&&r===u||(i=(n=l).copy()).on(s,r=u),h.on=i}}(this._id,t)):this.styleTween(t,function(t,e,n){var i,r,a=n+"";return function(){var o=it(this,t);return o===a?null:o===i?r:r=e(i=o,n)}}(t,i,e),n).on("end.style."+t,null)},styleTween:function(t,e,n){var i="style."+(t+="");if(arguments.length<2)return(i=this.tween(i))&&i._value;if(null==e)return this.tween(i,null);if("function"!=typeof e)throw new Error;return this.tween(i,function(t,e,n){var i,r;function a(){var a=e.apply(this,arguments);return a!==r&&(i=(r=a)&&function(t,e,n){return function(i){this.style.setProperty(t,e.call(this,i),n)}}(t,a,n)),i}return a._value=e,a}(t,e,null==n?"":n))},text:function(t){return this.tween("text","function"==typeof t?function(t){return function(){var e=t(this);this.textContent=null==e?"":e}}(xn(this,"text",t)):function(t){return function(){this.textContent=t}}(null==t?"":t+""))},textTween:function(t){var e="text";if(arguments.length<1)return(e=this.tween(e))&&e._value;if(null==t)return this.tween(e,null);if("function"!=typeof t)throw new Error;return this.tween(e,function(t){var e,n;function i(){var i=t.apply(this,arguments);return i!==n&&(e=(n=i)&&function(t){return function(e){this.textContent=t.call(this,e)}}(i)),e}return i._value=t,i}(t))},remove:function(){return this.on("end.remove",function(t){return function(){var e=this.parentNode;for(var n in this.__transition)if(+n!==t)return;e&&e.removeChild(this)}}(this._id))},tween:function(t,e){var n=this._id;if(t+="",arguments.length<2){for(var i,r=ve(this.node(),n).tween,a=0,o=r.length;a()=>t;function jn(t,{sourceEvent:e,target:n,transform:i,dispatch:r}){Object.defineProperties(this,{type:{value:t,enumerable:!0,configurable:!0},sourceEvent:{value:e,enumerable:!0,configurable:!0},target:{value:n,enumerable:!0,configurable:!0},transform:{value:i,enumerable:!0,configurable:!0},_:{value:r}})}function Vn(t,e,n){this.k=t,this.x=e,this.y=n}Vn.prototype={constructor:Vn,scale:function(t){return 1===t?this:new Vn(this.k*t,this.x,this.y)},translate:function(t,e){return 0===t&0===e?this:new Vn(this.k,this.x+this.k*t,this.y+this.k*e)},apply:function(t){return[t[0]*this.k+this.x,t[1]*this.k+this.y]},applyX:function(t){return t*this.k+this.x},applyY:function(t){return t*this.k+this.y},invert:function(t){return[(t[0]-this.x)/this.k,(t[1]-this.y)/this.k]},invertX:function(t){return(t-this.x)/this.k},invertY:function(t){return(t-this.y)/this.k},rescaleX:function(t){return t.copy().domain(t.range().map(this.invertX,this).map(t.invert,t))},rescaleY:function(t){return t.copy().domain(t.range().map(this.invertY,this).map(t.invert,t))},toString:function(){return"translate("+this.x+","+this.y+") scale("+this.k+")"}};var Gn=new Vn(1,0,0);function Fn(t){for(;!t.__zoom;)if(!(t=t.parentNode))return Gn;return t.__zoom}function Kn(t){t.stopImmediatePropagation()}function Qn(t){t.preventDefault(),t.stopImmediatePropagation()}function Zn(t){return!(t.ctrlKey&&"wheel"!==t.type||t.button)}function Jn(){var t=this;return t instanceof SVGElement?(t=t.ownerSVGElement||t).hasAttribute("viewBox")?[[(t=t.viewBox.baseVal).x,t.y],[t.x+t.width,t.y+t.height]]:[[0,0],[t.width.baseVal.value,t.height.baseVal.value]]:[[0,0],[t.clientWidth,t.clientHeight]]}function ti(){return this.__zoom||Gn}function ei(t){return-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002)*(t.ctrlKey?10:1)}function ni(){return navigator.maxTouchPoints||"ontouchstart"in this}function ii(t,e,n){var i=t.invertX(e[0][0])-n[0][0],r=t.invertX(e[1][0])-n[1][0],a=t.invertY(e[0][1])-n[0][1],o=t.invertY(e[1][1])-n[1][1];return t.translate(r>i?(i+r)/2:Math.min(0,i)||Math.max(0,r),o>a?(a+o)/2:Math.min(0,a)||Math.max(0,o))}function ri(){var t,e,n,i=Zn,r=Jn,a=ii,o=ei,s=ni,h=[0,1/0],l=[[-1/0,-1/0],[1/0,1/0]],u=250,c=yn,f=Bt("start","zoom","end"),d=500,p=150,g=0,_=10;function m(t){t.property("__zoom",ti).on("wheel.zoom",k,{passive:!1}).on("mousedown.zoom",A).on("dblclick.zoom",E).filter(s).on("touchstart.zoom",M).on("touchmove.zoom",$).on("touchend.zoom touchcancel.zoom",z).style("-webkit-tap-highlight-color","rgba(0,0,0,0)")}function y(t,e){return(e=Math.max(h[0],Math.min(h[1],e)))===t.k?t:new Vn(e,t.x,t.y)}function v(t,e,n){var i=e[0]-n[0]*t.k,r=e[1]-n[1]*t.k;return i===t.x&&r===t.y?t:new Vn(t.k,i,r)}function w(t){return[(+t[0][0]+ +t[1][0])/2,(+t[0][1]+ +t[1][1])/2]}function x(t,e,n,i){t.on("start.zoom",(function(){b(this,arguments).event(i).start()})).on("interrupt.zoom end.zoom",(function(){b(this,arguments).event(i).end()})).tween("zoom",(function(){var t=this,a=arguments,o=b(t,a).event(i),s=r.apply(t,a),h=null==n?w(s):"function"==typeof n?n.apply(t,a):n,l=Math.max(s[1][0]-s[0][0],s[1][1]-s[0][1]),u=t.__zoom,f="function"==typeof e?e.apply(t,a):e,d=c(u.invert(h).concat(l/u.k),f.invert(h).concat(l/f.k));return function(t){if(1===t)t=f;else{var e=d(t),n=l/e[2];t=new Vn(n,h[0]-e[0]*n,h[1]-e[1]*n)}o.zoom(null,t)}}))}function b(t,e,n){return!n&&t.__zooming||new N(t,e)}function N(t,e){this.that=t,this.args=e,this.active=0,this.sourceEvent=null,this.extent=r.apply(t,e),this.taps=0}function k(t,...e){if(i.apply(this,arguments)){var n=b(this,e).event(t),r=this.__zoom,s=Math.max(h[0],Math.min(h[1],r.k*Math.pow(2,o.apply(this,arguments)))),u=Lt(t);if(n.wheel)n.mouse[0][0]===u[0]&&n.mouse[0][1]===u[1]||(n.mouse[1]=r.invert(n.mouse[0]=u)),clearTimeout(n.wheel);else{if(r.k===s)return;n.mouse=[u,r.invert(u)],we(this),n.start()}Qn(t),n.wheel=setTimeout((function(){n.wheel=null,n.end()}),p),n.zoom("mouse",a(v(y(r,s),n.mouse[0],n.mouse[1]),n.extent,l))}}function A(t,...e){if(!n&&i.apply(this,arguments)){var r=t.currentTarget,o=b(this,e,!0).event(t),s=Dt(t.view).on("mousemove.zoom",(function(t){if(Qn(t),!o.moved){var e=t.clientX-u,n=t.clientY-c;o.moved=e*e+n*n>g}o.event(t).zoom("mouse",a(v(o.that.__zoom,o.mouse[0]=Lt(t,r),o.mouse[1]),o.extent,l))}),!0).on("mouseup.zoom",(function(t){s.on("mousemove.zoom mouseup.zoom",null),function(t,e){var n=t.document.documentElement,i=Dt(t).on("dragstart.drag",null);e&&(i.on("click.drag",qn,Yn),setTimeout((function(){i.on("click.drag",null)}),0)),"onselectstart"in n?i.on("selectstart.drag",null):(n.style.MozUserSelect=n.__noselect,delete n.__noselect)}(t.view,o.moved),Qn(t),o.event(t).end()}),!0),h=Lt(t,r),u=t.clientX,c=t.clientY;!function(t){var e=t.document.documentElement,n=Dt(t).on("dragstart.drag",qn,Yn);"onselectstart"in e?n.on("selectstart.drag",qn,Yn):(e.__noselect=e.style.MozUserSelect,e.style.MozUserSelect="none")}(t.view),Kn(t),o.mouse=[h,this.__zoom.invert(h)],we(this),o.start()}}function E(t,...e){if(i.apply(this,arguments)){var n=this.__zoom,o=Lt(t.changedTouches?t.changedTouches[0]:t,this),s=n.invert(o),h=n.k*(t.shiftKey?.5:2),c=a(v(y(n,h),o,s),r.apply(this,e),l);Qn(t),u>0?Dt(this).transition().duration(u).call(x,c,o,t):Dt(this).call(m.transform,c,o,t)}}function M(n,...r){if(i.apply(this,arguments)){var a,o,s,h,l=n.touches,u=l.length,c=b(this,r,n.changedTouches.length===u).event(n);for(Kn(n),o=0;o=0&&(a-=li(t,e)),null===t.coords&&(n.moveTo(t.spouse.x+r,a),n.lineTo(t.source.x-r,a)),t.coords&&t.coords.length>0){for(let e=0;e0&&(o=t.coords[e-1].x+r);let h=e>0?i:0,l=e+1<=t.coords.length?i:0;n.moveTo(o+h,a),n.lineTo(s-l,a)}n.moveTo(t.coords[t.coords.length-1].x+r+i,a),n.lineTo(t.source.x-r,a)}return n.toString()}(t,e)}function li(t,e){return(t.source.data.family-Math.ceil(t.spouse.data.spouses.length/2))*e.direction*5}class ui extends si{constructor(t,e){super(t,e),this._splittNames=!0}get direction(){return 1}get nodeWidth(){return this._boxWidth+this._xOffset}get nodeHeight(){return this._boxHeight+this._yOffset}norm(t){t.y*=this.direction}elbow(t){return hi(t,this)}}class ci extends si{constructor(t,e){super(t,e),this._splittNames=!0}get direction(){return-1}get nodeWidth(){return this._boxWidth+this._xOffset}get nodeHeight(){return this._boxHeight+this._yOffset}norm(t){t.y*=this.direction}elbow(t){return hi(t,this)}}function fi(t,e){const n=e.xOffset/2,i=e.yOffset/2;let r=t.source.x,a=t.source.y;if(void 0!==t.spouse&&0===t.source.data.family?(r-=di(t,e),a-=(t.source.y-t.spouse.y)/2):r+=e.boxWidth/2*e.direction,null===t.source.data.data&&(r+=e.boxWidth/2*e.direction,a-=e.boxHeight/2+i/2),null!==t.target){let i=t.target.x-e.direction*(e.boxWidth/2+n),o=t.target.y;const s=b();return s.moveTo(r,a),s.lineTo(i,a),s.lineTo(i,o),s.lineTo(i+e.direction*n,o),s.toString()}return function(t,e){const n=b(),i=2,r=e.boxHeight/2;let a=t.source.x;if(t.spouse.data.spouses.length>=0&&(a-=di(t,e)),null===t.coords&&(n.moveTo(a,t.spouse.y+r),n.lineTo(a,t.source.y-r)),t.coords&&t.coords.length>0){for(let e=0;e0&&(o=t.coords[e-1].y+r);let h=e>0?i:0,l=e+1<=t.coords.length?i:0;n.moveTo(a,o+h),n.lineTo(a,s-l)}n.moveTo(a,t.coords[t.coords.length-1].y+r+i),n.lineTo(a,t.source.y-r)}return n.toString()}(t,e)}function di(t,e){return(t.source.data.family-Math.ceil(t.spouse.data.spouses.length/2))*e.direction*5}class pi extends si{constructor(t,e){super(t,e),this._xOffset=40,this._yOffset=20}get direction(){return this.isDocumentRtl?-1:1}get nodeWidth(){return this._boxHeight+this._yOffset}get nodeHeight(){return this._boxWidth+this._xOffset}norm(t){[t.x,t.y]=[t.y*this.direction,t.x]}elbow(t){return fi(t,this)}}class gi extends si{constructor(t,e){super(t,e),this._xOffset=40,this._yOffset=20}get direction(){return this.isDocumentRtl?1:-1}get nodeWidth(){return this._boxHeight+this._yOffset}get nodeHeight(){return this._boxWidth+this._xOffset}norm(t){[t.x,t.y]=[t.y*this.direction,t.x]}elbow(t){return fi(t,this)}}class _i{constructor(){this._orientations={down:new ui(160,205),up:new ci(160,205),[ai]:new pi(325,95),left:new gi(325,95)}}get(){return this._orientations}}class mi{constructor(t,e=4,n=!1,i=ai,r=!1,a=1){this._treeLayout=i,this._orientations=new _i,this.duration=750,this.padding=15,this.imagePadding=5,this._generations=e,this.textPadding=8,this._fontSize=14,this.fontColor="rgb(0, 0, 0)",this._showEmptyBoxes=n,this.rtl=r,this.labels=t,this.direction=a}get generations(){return this._generations}set generations(t){this._generations=t}get showEmptyBoxes(){return this._showEmptyBoxes}set showEmptyBoxes(t){this._showEmptyBoxes=t}get treeLayout(){return this._treeLayout}set treeLayout(t){this._treeLayout=t}get orientation(){return this._orientations.get()[this.treeLayout]}}class yi{constructor(t){this._configuration=t,this._nodes=null,this._root=null}init(t){const e=({parents:t})=>1+(t?Math.max(...t.map(e)):0),n=e(t);this._root=i(t,(t=>this._configuration.showEmptyBoxes?(!t.parents&&t.data.generation{t.id=e}));const r=g().nodeSize([this._configuration.orientation.nodeWidth,this._configuration.orientation.nodeHeight]).separation((()=>1));this._nodes=r(this._root),this._root.each((t=>{this._configuration.orientation.norm(t)}))}get nodes(){return this._nodes}get root(){return this._root}createEmptyNode(t,e){return{data:{id:0,xref:"",url:"",updateUrl:"",generation:t,name:"",isNameRtl:!1,firstNames:[],lastNames:[],preferredName:"",alternativeName:"",isAltRtl:!1,sex:"U",timespan:""}}}}class vi{constructor(t){this._element=t.append("defs")}get(){return this._element}}class wi{constructor(t){this._zoom=null,this._parent=t,this.init()}init(){this._zoom=ri(),this._zoom.scaleExtent([.1,20]).on("zoom",(t=>{this._parent.attr("transform",t.transform)})),this._zoom.wheelDelta((t=>-t.deltaY*(1===t.deltaMode?.05:t.deltaMode?1:.002))),this._zoom.filter((t=>{if("wheel"===t.type){if(!t.ctrlKey)return!1;const e=Fn(this);if(e.k){if(e.k<=.1&&t.deltaY>0)return t.preventDefault(),!1;if(e.k>=20&&t.deltaY<0)return t.preventDefault(),!1}return!0}return t.button||"touchstart"!==t.type?!(t.ctrlKey&&"wheel"!==t.type||t.button):2===t.touches.length}))}get(){return this._zoom}}class xi{triggerDownload(t,e){let n=new MouseEvent("click",{view:window,bubbles:!1,cancelable:!0}),i=document.createElement("a");i.setAttribute("download",e),i.setAttribute("href",t),i.setAttribute("target","_blank"),i.dispatchEvent(n)}}class bi extends xi{copyStylesInline(t,e){let n=["svg","g","text","textPath"];for(let i=0;i{let r=(new XMLSerializer).serializeToString(t),a=window.URL||window.webkitURL||window,o=new Blob([r],{type:"image/svg+xml;charset=utf-8"}),s=a.createObjectURL(o),h=new Image;h.onload=()=>{let t=this.createCanvas(e,n),r=t.getContext("2d");r.fillStyle="rgb(255,255,255)",r.fillRect(0,0,t.width,t.height),r.drawImage(h,0,0),a.revokeObjectURL(s);let o=t.toDataURL("image/png").replace("image/png","image/octet-stream");i(o)},h.src=s}))}cloneSvg(t){return new Promise((e=>{e(t.cloneNode(!0))}))}svgToImage(t,e){const n=[4960,3508];this.cloneSvg(t.get().node()).then((e=>{this.copyStylesInline(t.get().node(),e);const i=this.calculateViewBox(t.get().node()),r=Math.max(n[0],i[2]),a=Math.max(n[1],i[3]);return e.setAttribute("width",""+r),e.setAttribute("height",""+a),e.setAttribute("viewBox",""+i),this.convertToDataUrl(e,r,a)})).then((t=>this.triggerDownload(t,e))).catch((()=>{console.log("Failed to save chart as PNG image")}))}}class Ni extends xi{copyStylesInline(t,n,i){return new Promise((r=>{Promise.all(t.map((t=>function(t,n){return fetch(t,n).then(e)}(t)))).then((t=>{t.forEach((t=>{t=t.replace(new RegExp("."+i+" ","g"),"");let e=document.createElementNS("http://www.w3.org/2000/svg","style");e.appendChild(document.createTextNode(t)),n.prepend(e)})),n.classList.add("wt-global"),r(n)}))}))}convertToObjectUrl(t){return new Promise((e=>{let n=(new XMLSerializer).serializeToString(t),i=window.URL||window.webkitURL||window,r=new Blob([n],{type:"image/svg+xml;charset=utf-8"}),a=i.createObjectURL(r),o=new Image;o.onload=()=>{e(a)},o.src=a}))}cloneSvg(t){return new Promise((e=>{e(t.cloneNode(!0))}))}svgToImage(t,e,n,i){this.cloneSvg(t.get().node()).then((t=>this.copyStylesInline(e,t,n))).then((t=>this.convertToObjectUrl(t))).then((t=>this.triggerDownload(t,i))).catch((()=>{console.log("Failed to save chart as SVG image")}))}}class ki{constructor(){this._exportClass=null}setExportClass(t){switch(t){case"png":this._exportClass=bi;break;case"svg":this._exportClass=Ni}}createExport(t){switch(this.setExportClass(t),t){case"png":case"svg":return new this._exportClass}}}class Ai{constructor(t,e){this._element=t.append("svg"),this._defs=new vi(this._element),this._visual=null,this._zoom=null,this._configuration=e,this.init()}init(){this._element.attr("width","100%").attr("height","100%").attr("text-rendering","optimizeLegibility").attr("text-anchor","middle").attr("xmlns:xlink","https://www.w3.org/1999/xlink")}initEvents(t){this._element.on("contextmenu",(t=>t.preventDefault())).on("wheel",(e=>{e.ctrlKey||t.show(this._configuration.labels.zoom,300,(()=>{t.hide(200,600)}))})).on("touchend",(e=>{e.touches.length<2&&t.hide(0,600)})).on("touchmove",(e=>{e.touches.length>=2?t.hide():t.show(this._configuration.labels.move)})).on("click",(t=>this.doStopPropagation(t)),!0),this._configuration.rtl&&this._element.classed("rtl",!0),this._visual=this._element.append("g"),this._zoom=new wi(this._visual),this._element.call(this._zoom.get())}doStopPropagation(t){t.defaultPrevented&&t.stopPropagation()}export(t){return(new ki).createExport(t)}get defs(){return this._defs}get zoom(){return this._zoom}get visual(){return this._visual}get(){return this._element}}class Ei{constructor(t){this._element=t.append("div").attr("class","overlay").style("opacity",1e-6)}show(t,e=0,n=null){this._element.select("p").remove(),this._element.append("p").attr("class","tooltip").text(t),this._element.transition().duration(e).style("opacity",1).on("end",(()=>{"function"==typeof n&&n()}))}hide(t=0,e=0){this._element.transition().delay(t).duration(e).style("opacity",1e-6)}get(){return this._element}}let Mi=null;function $i(t,e,n,i=400){null===Mi&&(Mi=document.createElement("canvas"));const r=Mi.getContext("2d"),a=`${i||""} ${n} ${e}`;return r.font!==a&&(r.font=a),r.measureText(t).width}class zi{constructor(t,e,n,i){this._svg=t,this._orientation=e,this._image=n,this._text=i}appendName(t){const e=t.append("g").attr("class","name");if(this._orientation instanceof ui||this._orientation instanceof ci){const t=e.selectAll("text").data((t=>[{data:t.data,isRtl:t.data.data.isNameRtl,isAltRtl:t.data.data.isAltRtl,withImage:!0}])).enter();t.call((t=>{const e=t.append("text").attr("class","wt-chart-box-name").attr("text-anchor","middle").attr("direction",(t=>t.isRtl?"rtl":"ltr")).attr("alignment-baseline","central").attr("y",this._text.y-5);this.addNameElements(e,(t=>this.createNamesData(e,t,!0,!1)))})).call((t=>{const e=t.append("text").attr("class","wt-chart-box-name").attr("text-anchor","middle").attr("direction",(t=>t.isRtl?"rtl":"ltr")).attr("alignment-baseline","central").attr("y",this._text.y+15);this.addNameElements(e,(t=>this.createNamesData(e,t,!1,!0)))})),t.filter((t=>""!==t.data.data.alternativeName)).call((t=>{const e=t.append("text").attr("class","wt-chart-box-name").attr("text-anchor","middle").attr("direction",(t=>t.isAltRtl?"rtl":"ltr")).attr("alignment-baseline","central").attr("y",this._text.y+37).classed("wt-chart-box-name-alt",!0);this.addNameElements(e,(t=>this.createAlternativeNamesData(e,t)))}))}else{const t=e.selectAll("text").data((t=>[{data:t.data,isRtl:t.data.data.isNameRtl,isAltRtl:t.data.data.isAltRtl,withImage:""!==t.data.data.thumbnail}])).enter();t.call((t=>{const e=t.append("text").attr("class","wt-chart-box-name").attr("text-anchor",(t=>t.isRtl&&this._orientation.isDocumentRtl?"start":t.isRtl||this._orientation.isDocumentRtl?"end":"start")).attr("direction",(t=>t.isRtl?"rtl":"ltr")).attr("x",(t=>this.textX(t))).attr("y",this._text.y-10);this.addNameElements(e,(t=>this.createNamesData(e,t,!0,!0)))})),t.filter((t=>""!==t.data.data.alternativeName)).call((t=>{const e=t.append("text").attr("class","wt-chart-box-name").attr("text-anchor",(t=>t.isAltRtl&&this._orientation.isDocumentRtl?"start":t.isAltRtl||this._orientation.isDocumentRtl?"end":"start")).attr("direction",(t=>t.isAltRtl?"rtl":"ltr")).attr("x",(t=>this.textX(t))).attr("y",this._text.y+8).classed("wt-chart-box-name-alt",!0);this.addNameElements(e,(t=>this.createAlternativeNamesData(e,t)))}))}}addNameElements(t,e){t.selectAll("tspan").data(e).enter().call((t=>{t.append("tspan").text((t=>t.label)).attr("dx",((t,e)=>0!==e?.25*(t.isNameRtl?-1:1)+"em":null)).classed("preferred",(t=>t.isPreferred)).classed("lastName",(t=>t.isLastName))}))}createNamesData(t,e,n,i){let r=[];!0===n&&(r=r.concat(e.data.data.firstNames.map((t=>({label:t,isPreferred:t===e.data.data.preferredName,isLastName:!1,isNameRtl:e.data.data.isNameRtl}))))),!0===i&&(r=r.concat(e.data.data.lastNames.map((t=>({label:t,isPreferred:!1,isLastName:!0,isNameRtl:e.data.data.isNameRtl})))));const a=t.style("font-size"),o=t.style("font-weight");let s=this._text.width;return e.withImage&&(this._orientation instanceof pi||this._orientation instanceof gi)&&(s-=this._image.width),this.truncateNames(r,a,o,s)}createAlternativeNamesData(t,e){let n=e.data.data.alternativeName.split(/\s+/),i=[];i=i.concat(n.map((t=>({label:t,isPreferred:!1,isLastName:!1,isNameRtl:e.data.data.isAltRtl}))));const r=t.style("font-size"),a=t.style("font-weight");let o=this._text.width;return e.withImage&&(this._orientation instanceof pi||this._orientation instanceof gi)&&(o-=this._image.width),this.truncateNames(i,r,a,o)}truncateNames(t,e,n,i){let r=t.map((t=>t.label)).join(" ");return t.reverse().map((a=>(!1===a.isPreferred&&!1===a.isLastName&&this.measureText(r,e,n)>i&&(a.label=a.label.slice(0,1)+".",r=t.map((t=>t.label)).join(" ")),a))).map((a=>(!0===a.isPreferred&&this.measureText(r,e,n)>i&&(a.label=a.label.slice(0,1)+".",r=t.map((t=>t.label)).join(" ")),a))).map((a=>(!0===a.isLastName&&this.measureText(r,e,n)>i&&(a.label=a.label.slice(0,1)+".",r=t.map((t=>t.label)).join(" ")),a))).reverse()}textX(t){const e=this._text.x+(t.withImage?this._image.width:0);return this._orientation.isDocumentRtl?-e:e}measureText(t,e,n=400){return $i(t,this._svg.get().style("font-family"),e,n)}}class Ti{constructor(t,e=null){this._orientation=t,this._image=e,this._textPaddingX=15,this._textPaddingY=15,(this._orientation instanceof ui||this._orientation instanceof ci)&&(this._textPaddingX=5,this._textPaddingY=15),this._x=this.calculateX(),this._y=this.calculateY(),this._width=this.calculateWidth()}calculateX(){return-this._orientation.boxWidth/2+this._textPaddingX}calculateY(){return this._orientation instanceof pi||this._orientation instanceof gi?-this._textPaddingY:this._image.y+this._image.height+2*this._textPaddingY}calculateWidth(){return this._orientation.boxWidth-2*this._textPaddingX}get x(){return this._x}get y(){return this._y}get width(){return this._width}}class Ri{constructor(t,e,n){this._svg=t,this._hierarchy=e,this._configuration=n,this._orientation=this._configuration.orientation,this._image=new class{constructor(t,e){this._orientation=t,this._cornerRadius=e,this._imagePadding=5,this._imageRadius=Math.min(40,this._orientation.boxHeight/2-this._imagePadding),this._width=this.calculateImageWidth(),this._height=this.calculateImageHeight(),this._rx=this.calculateCornerRadius(),this._ry=this.calculateCornerRadius(),this._x=this.calculateX(),this._y=this.calculateY()}calculateX(){return this._orientation instanceof pi||this._orientation instanceof gi?this._orientation.isDocumentRtl?this._width-this._imagePadding:-(this._orientation.boxWidth-this._imagePadding)/2+this._imagePadding:-this._orientation.boxWidth/2+this._width/2}calculateY(){return this._orientation instanceof pi||this._orientation instanceof gi?-this._imageRadius:-(this._orientation.boxHeight-this._imagePadding)/2+this._imagePadding}calculateImageWidth(){return 2*this._imageRadius}calculateImageHeight(){return 2*this._imageRadius}calculateCornerRadius(){return this._cornerRadius-this._imagePadding}get x(){return this._x}get y(){return this._y}get rx(){return this._rx}get ry(){return this._ry}get width(){return this._width}get height(){return this._height}}(this._orientation,20),this._text=new Ti(this._orientation,this._image),this._name=new zi(this._svg,this._orientation,this._image,this._text),this._date=new class{constructor(t,e,n,i){this._svg=t,this._orientation=e,this._image=n,this._text=i}appendDate(t){const e=t.append("g").attr("class","table");if(this._orientation instanceof ui||this._orientation instanceof ci){const t=e.selectAll("text.date").data((t=>[{label:t.data.data.timespan,withImage:!0}])).enter().append("text").attr("class","date").attr("text-anchor","middle").attr("alignment-baseline","central").attr("y",this._text.y+75);t.append("title").text((t=>t.label));const n=t.append("tspan");n.text((t=>this.truncateDate(n,t.label,this._text.width)))}else e.selectAll("text").data((t=>{let e=[];return t.data.data.birth&&e.push({icon:"★",label:t.data.data.birth,birth:!0,withImage:""!==t.data.data.thumbnail}),t.data.data.death&&e.push({icon:"†",label:t.data.data.death,death:!0,withImage:""!==t.data.data.thumbnail}),e})).enter().call((t=>{t.append("text").attr("fill","currentColor").attr("text-anchor","middle").attr("dominant-baseline","middle").attr("x",(t=>this.textX(t))).attr("y",((t,e)=>this._text.y+30+(0===e?0:21))).append("tspan").text((t=>t.icon)).attr("dx",5*(this._orientation.isDocumentRtl?-1:1));const e=t.append("text").attr("class","date").attr("text-anchor","start").attr("dominant-baseline","middle").attr("x",(t=>this.textX(t))).attr("y",((t,e)=>this._text.y+30+(0===e?0:20)));e.append("title").text((t=>t.label));const n=e.append("tspan");n.text((t=>this.truncateDate(n,t.label,this._text.width-(t.withImage?this._image.width:0)-25))).attr("dx",15*(this._orientation.isDocumentRtl?-1:1))}))}truncateDate(t,e,n){const i=t.style("font-size"),r=t.style("font-weight");let a=!1;for(;this.measureText(e,i,r)>n&&e.length>1;)e=e.slice(0,-1).trim(),a=!0;return"."===e[e.length-1]&&(e=e.slice(0,-1).trim()),a?e+"…":e}textX(t){const e=this._text.x+(t.withImage?this._image.width:0);return this._orientation.isDocumentRtl?-e:e}measureText(t,e,n=400){return $i(t,this._svg.get().style("font-family"),e,n)}}(this._svg,this._orientation,this._image,this._text)}drawNodes(t,e){this._svg.defs.get().append("clipPath").attr("id","clip-image").append("rect").attr("rx",this._image.rx).attr("ry",this._image.ry).attr("x",this._image.x).attr("y",this._image.y).attr("width",this._image.width).attr("height",this._image.height),this._svg.visual.selectAll("g.person").data(t,(t=>t.id)).join((t=>this.nodeEnter(t,e)),(t=>this.nodeUpdate(t)),(t=>this.nodeExit(t,e))),this._hierarchy.root.eachBefore((t=>{t.x0=t.x,t.y0=t.y}))}nodeEnter(t,e){t.append("g").attr("opacity",0).attr("class",(t=>"person"+(t.data.spouse?" spouse":""))).attr("transform",(t=>"translate("+t.x+","+t.y+")")).call((t=>{t.append("rect").attr("class",(t=>"F"===t.data.data.sex?"female":t.data.data.sex===oi?"male":"unknown")).attr("rx",20).attr("ry",20).attr("x",-this._orientation.boxWidth/2).attr("y",-this._orientation.boxHeight/2).attr("width",this._orientation.boxWidth).attr("height",this._orientation.boxHeight).attr("fill-opacity",.5),t.append("title").text((t=>t.data.data.name))})).call((t=>this.drawNode(t))).call((t=>t.transition().duration(this._configuration.duration).attr("opacity",1)))}nodeUpdate(t){t.call((t=>t.transition().duration(this._configuration.duration).attr("opacity",1).attr("transform",(t=>"translate("+t.x+","+t.y+")"))))}nodeExit(t,e){t.call((t=>t.transition().duration(this._configuration.duration).attr("opacity",0).attr("transform",(()=>"translate("+e.x0+","+e.y0+")")).remove()))}drawNode(t){const e=t.selectAll("g.image").data((t=>{let e=[];return t.data.data.thumbnail&&e.push({image:t.data.data.thumbnail}),e})).enter().append("g").attr("class","image");e.append("rect").attr("x",this._image.x).attr("y",this._image.y).attr("width",this._image.width).attr("height",this._image.height).attr("rx",this._image.rx).attr("ry",this._image.ry).attr("fill","rgb(255, 255, 255)"),e.append("image").attr("x",this._image.x).attr("y",this._image.y).attr("width",this._image.width).attr("height",this._image.height).attr("clip-path","url(#clip-image)"),e.append("rect").attr("x",this._image.x).attr("y",this._image.y).attr("width",this._image.width).attr("height",this._image.height).attr("rx",this._image.rx).attr("ry",this._image.ry).attr("fill","none").attr("stroke","rgb(200, 200, 200)").attr("stroke-width",1.5),function(t){return"string"==typeof t?new Pt([document.querySelectorAll(t)],[document.documentElement]):new Pt([R(t)],St)}("g.image image").each((function(t){let e=Dt(this);(function(t,e=null){return fetch(t,e).then((t=>t.blob())).then((t=>new Promise(((e,n)=>{const i=new FileReader;i.onloadend=()=>e(i.result),i.onerror=n,i.readAsDataURL(t)}))))})(t.image).then((t=>e.attr("xlink:href",t))).catch((t=>{console.error(t)}))})),this._name.appendName(t),this._date.appendDate(t)}}class Si{constructor(t,e){this._svg=t,this._configuration=e,this._orientation=this._configuration.orientation}drawLinks(t,e){this._svg.visual.selectAll("path.link").data(t).join((t=>this.linkEnter(t,e)),(t=>this.linkUpdate(t)),(t=>this.linkExit(t,e)))}linkEnter(t,e){t.append("path").classed("link",!0).attr("d",(t=>this._orientation.elbow(t))).call((t=>t.transition().duration(this._configuration.duration).attr("opacity",1)))}linkUpdate(t){}linkExit(t,e){}}class Pi{constructor(t,e,n){this._svg=t,this._configuration=e,this._hierarchy=n,this._hierarchy.root.x0=0,this._hierarchy.root.y0=0,this._orientation=this._configuration.orientation,this._nodeDrawer=new Ri(this._svg,this._hierarchy,this._configuration),this._linkDrawer=new Si(this._svg,this._configuration),this.draw(this._hierarchy.root)}draw(t){const e=this._hierarchy.root.descendants(),n=this._hierarchy.nodes.links();this._linkDrawer.drawLinks(n,t),this._nodeDrawer.drawNodes(e,t)}centerTree(){console.log("centerTree")}togglePerson(t,e){e.parents?(e._parents=e.parents,e.parents=null):(e.parents=e._parents,e._parents=null),this.draw(e)}collapse(t){t.parents&&(t._parents=t.parents,t._parents.forEach((t=>this.collapse(t))),t.parents=null)}}class Ci{constructor(t,e){this._configuration=e,this._parent=t,this._hierarchy=new yi(this._configuration),this._data={}}get svg(){return this._svg}updateViewBox(){let t=this._svg.visual.node().getBBox(),e=this._parent.node().getBoundingClientRect(),n=Math.max(e.width,t.width),i=Math.max(e.height,t.height,300),r=(n-t.width)/2,a=(i-t.height)/2,o=Math.ceil(t.x-r-10),s=Math.ceil(t.y-a-10);n=Math.ceil(n+20),i=Math.ceil(i+20),this._svg.get().attr("viewBox",[o,s,n,i])}get data(){return this._data}set data(t){this._data=t,this._hierarchy.init(this._data)}draw(){this._parent.html(""),this._svg=new Ai(this._parent,this._configuration),this._overlay=new Ei(this._parent),this._svg.initEvents(this._overlay),new Pi(this._svg,this._configuration,this._hierarchy),this.bindClickEventListener(),this.updateViewBox()}bindClickEventListener(){let t=this;this._svg.visual.selectAll("g.person").filter((t=>""!==t.data.data.xref)).each((function(e){Dt(this).on("click",(function(){t.personClick(e.data)}))}))}personClick(t){1===t.data.generation?this.redirectToIndividual(t.data.url):this.update(t.data.updateUrl)}redirectToIndividual(t){window.open(t,"_blank")}update(t){window.location=t}}t.PedigreeChart=class{constructor(t,e){this._selector=t,this._parent=Dt(this._selector),this._configuration=new mi(e.labels,e.generations,e.showEmptyBoxes,e.treeLayout,e.rtl),this._cssFiles=e.cssFiles,this._chart=new Ci(this._parent,this._configuration),this.init(),this.draw(e.data)}init(){Dt("#centerButton").on("click",(()=>this.center())),Dt("#exportPNG").on("click",(()=>this.exportPNG())),Dt("#exportSVG").on("click",(()=>this.exportSVG()))}center(){this._chart.svg.get().transition().duration(750).call(this._chart.svg.zoom.get().transform,Gn)}get configuration(){return this._configuration}update(t){this._chart.update(t)}draw(t){this._chart.data=t,this._chart.draw()}exportPNG(){this._chart.svg.export("png").svgToImage(this._chart.svg,"pedigree-chart.png")}exportSVG(){this._chart.svg.export("svg").svgToImage(this._chart.svg,this._cssFiles,"webtrees-pedigree-chart-container","pedigree-chart.svg")}}},"object"==typeof exports&&"undefined"!=typeof module?e(exports):"function"==typeof define&&define.amd?define(["exports"],e):e((t="undefined"!=typeof globalThis?globalThis:t||self).WebtreesPedigreeChart={}); diff --git a/resources/views/layouts/ajax.phtml b/resources/views/layouts/ajax.phtml index 463ff82..420a4d2 100644 --- a/resources/views/layouts/ajax.phtml +++ b/resources/views/layouts/ajax.phtml @@ -4,7 +4,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ declare(strict_types=1); diff --git a/resources/views/modules/charts/chart.phtml b/resources/views/modules/charts/chart.phtml index 027d219..d1a95b2 100644 --- a/resources/views/modules/charts/chart.phtml +++ b/resources/views/modules/charts/chart.phtml @@ -4,7 +4,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ declare(strict_types=1); diff --git a/resources/views/modules/pedigree-chart/chart.phtml b/resources/views/modules/pedigree-chart/chart.phtml index e5f34a2..00fbaa8 100644 --- a/resources/views/modules/pedigree-chart/chart.phtml +++ b/resources/views/modules/pedigree-chart/chart.phtml @@ -4,7 +4,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ declare(strict_types=1); diff --git a/resources/views/modules/pedigree-chart/page.phtml b/resources/views/modules/pedigree-chart/page.phtml index f2eb55c..1ba8b82 100644 --- a/resources/views/modules/pedigree-chart/page.phtml +++ b/resources/views/modules/pedigree-chart/page.phtml @@ -4,7 +4,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ declare(strict_types=1); diff --git a/rollup.config.js b/rollup.config.js index bb95e00..4fc391c 100644 --- a/rollup.config.js +++ b/rollup.config.js @@ -40,7 +40,7 @@ export default [ // pedigree-chart-storage.js { - input: "resources/js/modules/storage.js", + input: "resources/js/modules/lib/storage.js", output: [ { name: "WebtreesPedigreeChart", @@ -53,7 +53,7 @@ export default [ ] }, { - input: "resources/js/modules/storage.js", + input: "resources/js/modules/lib/storage.js", output: [ { name: "WebtreesPedigreeChart", diff --git a/src/Configuration.php b/src/Configuration.php index 7868472..164b992 100644 --- a/src/Configuration.php +++ b/src/Configuration.php @@ -4,7 +4,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ declare(strict_types=1); diff --git a/src/Facade/DataFacade.php b/src/Facade/DataFacade.php new file mode 100644 index 0000000..5d1abed --- /dev/null +++ b/src/Facade/DataFacade.php @@ -0,0 +1,244 @@ + + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-pedigree-chart/ + */ +class DataFacade +{ + /** + * The module. + * + * @var ModuleCustomInterface + */ + private ModuleCustomInterface $module; + + /** + * The configuration instance. + * + * @var Configuration + */ + private Configuration $configuration; + + /** + * @var string + */ + private string $route; + + /** + * @param ModuleCustomInterface $module + * + * @return DataFacade + */ + public function setModule(ModuleCustomInterface $module): DataFacade + { + $this->module = $module; + return $this; + } + + /** + * @param Configuration $configuration + * + * @return DataFacade + */ + public function setConfiguration(Configuration $configuration): DataFacade + { + $this->configuration = $configuration; + return $this; + } + + /** + * @param string $route + * + * @return DataFacade + */ + public function setRoute(string $route): DataFacade + { + $this->route = $route; + return $this; + } + + /** + * Creates the JSON tree structure. + * + * @param Individual $individual + * + * @return null|Node + */ + public function createTreeStructure(Individual $individual): ?Node + { + return $this->buildTreeStructure($individual); + } + + /** + * Recursively build the data array of the individual ancestors. + * + * @param null|Individual $individual The start person + * @param int $generation The current generation + * + * @return null|Node + */ + private function buildTreeStructure(?Individual $individual, int $generation = 1): ?Node + { + // Maximum generation reached + if (($individual === null) || ($generation > $this->configuration->getGenerations())) { + return null; + } + + $node = new Node( + $this->getNodeData($generation, $individual) + ); + + /** @var null|Family $family */ + $family = $individual->childFamilies()->first(); + + if ($family === null) { + return $node; + } + + // Recursively call the method for the parents of the individual + $fatherTree = $this->buildTreeStructure($family->husband(), $generation + 1); + $motherTree = $this->buildTreeStructure($family->wife(), $generation + 1); + + // Add an array of child nodes + if ($fatherTree !== null) { + $node->addParent($fatherTree); +// $data['parents'][] = $fatherTree; + } + + if ($motherTree !== null) { + $node->addParent($motherTree); +// $data['parents'][] = $motherTree; + } + + return $node; + } + + /** + * Get the node data required for display the chart. + * + * @param int $generation The generation the person belongs to + * @param null|Individual $individual The current individual + * + * @return NodeData + */ + private function getNodeData( + int $generation, + Individual $individual + ): NodeData { + // Create a unique ID for each individual + static $id = 0; + + $treeData = new NodeData(); + $treeData->setId(++$id) + ->setGeneration($generation); + + $nameProcessor = new NameProcessor( + $individual, + null, + false +// $this->configuration->getShowMarriedNames() + ); + + $dateProcessor = new DateProcessor($individual); + $imageProcessor = new ImageProcessor($this->module, $individual); + + $fullNN = $nameProcessor->getFullName(); + $alternativeName = $nameProcessor->getAlternateName($individual); + + $treeData + ->setXref($individual->xref()) + ->setUrl($individual->url()) + ->setUpdateUrl($this->getUpdateRoute($individual)) + ->setName($fullNN) + ->setIsNameRtl($this->isRtl($fullNN)) + ->setFirstNames($nameProcessor->getFirstNames()) + ->setLastNames($nameProcessor->getLastNames()) + ->setPreferredName($nameProcessor->getPreferredName()) + ->setAlternativeName($alternativeName) + ->setIsAltRtl($this->isRtl($alternativeName)) + ->setThumbnail($imageProcessor->getHighlightImageUrl()) + ->setSex($individual->sex()) + ->setBirth($dateProcessor->getBirthDate()) + ->setDeath($dateProcessor->getDeathDate()) + ->setTimespan($dateProcessor->getLifetimeDescription()) + ->setIndividual($individual); + + return $treeData; + } + + /** + * Get the raw update URL. The "xref" parameter must be the last one as the URL gets appended + * with the clicked individual id in order to load the required chart data. + * + * @param Individual $individual + * + * @return string + */ + private function getUpdateRoute(Individual $individual): string + { + return $this->chartUrl( + $individual, + [ + 'generations' => $this->configuration->getGenerations(), + 'layout' => $this->configuration->getLayout(), + ] + ); + } + + /** + * @param Individual $individual + * @param array $parameters + * + * @return string + */ + private function chartUrl( + Individual $individual, + array $parameters = [] + ): string { + return route( + $this->route, + [ + 'xref' => $individual->xref(), + 'tree' => $individual->tree()->name(), + ] + $parameters + ); + } + + /** + * Returns whether the given text is in RTL style or not. + * + * @param string $text The text to check + * + * @return bool + */ + private function isRtl(string $text): bool + { + return I18N::scriptDirection(I18N::textScript($text)) === 'rtl'; + } +} diff --git a/src/Model/Node.php b/src/Model/Node.php new file mode 100644 index 0000000..16d0c95 --- /dev/null +++ b/src/Model/Node.php @@ -0,0 +1,83 @@ + + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-pedigree-chart/ + */ +class Node implements JsonSerializable +{ + /** + * @var NodeData + */ + protected NodeData $data; + + /** + * The list of parents. + * + * @var Node[] + */ + protected array $parents = []; + + /** + * Constructor. + * + * @param NodeData $data + */ + public function __construct(NodeData $data) + { + $this->data = $data; + } + + /** + * @return NodeData + */ + public function getData(): NodeData + { + return $this->data; + } + + /** + * @param Node $parent + * + * @return Node + */ + public function addParent(Node $parent): Node + { + $this->parents[] = $parent; + return $this; + } + + /** + * Returns the relevant data as an array. + * + * @return array + */ + public function jsonSerialize(): array + { + $jsonData = [ + 'data' => $this->data, + ]; + + if (count($this->parents) > 0) { + $jsonData['parents'] = $this->parents; + } + + return $jsonData; + } +} diff --git a/src/Model/NodeData.php b/src/Model/NodeData.php new file mode 100644 index 0000000..a58fb1e --- /dev/null +++ b/src/Model/NodeData.php @@ -0,0 +1,401 @@ + + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-pedigree-chart/ + */ +class NodeData implements JsonSerializable +{ + /** + * The unique ID of the individual. + * + * @var int + */ + protected int $id = 0; + + /** + * The XREF of the individual. + * + * @var string + */ + protected string $xref = ''; + + /** + * The URL to this individual in webtrees. + * + * @var string + */ + protected string $url = ''; + + /** + * The URL used to update the clicked entry in the tree with this individual. + * + * @var string + */ + protected string $updateUrl = ''; + + /** + * The generation the individual belongs to. + * + * @var int + */ + protected int $generation = 0; + + /** + * The full name of the individual. + * + * @var string + */ + protected string $name = ''; + + /** + * TRUE if the name is written right to left. + * + * @var bool + */ + protected bool $isNameRtl = false; + + /** + * The list of first names. + * + * @var string[] + */ + protected array $firstNames = []; + + /** + * The list of last names. + * + * @var string[] + */ + protected array $lastNames = []; + + /** + * The extracted preferred name. + * + * @var string + */ + protected string $preferredName = ''; + + /** + * The alternative name. + * + * @var string + */ + protected string $alternativeName = ''; + + /** + * TRUE if the alternative name is written right to left. + * + * @var bool + */ + protected bool $isAltRtl = false; + + /** + * The URL of the individuals highlight image. + * + * @var string + */ + protected string $thumbnail = ''; + + /** + * The sex of the individual. + * + * @var string + */ + protected string $sex = 'U'; + + /** + * The formatted birthdate without HTML tags. + * + * @var string + */ + protected string $birth = ''; + + /** + * The formatted death date without HTML tags. + * + * @var string + */ + protected string $death = ''; + + /** + * The timespan label. + * + * @var string + */ + protected string $timespan = ''; + + /** + * The underlying individual instance. Only used internally. + * + * @var null|Individual + */ + protected ?Individual $individual = null; + + /** + * @return int + */ + public function getId(): int + { + return $this->id; + } + + /** + * @param int $id + * + * @return NodeData + */ + public function setId(int $id): NodeData + { + $this->id = $id; + return $this; + } + + /** + * @param string $xref + * + * @return NodeData + */ + public function setXref(string $xref): NodeData + { + $this->xref = $xref; + return $this; + } + + /** + * @param string $url + * + * @return NodeData + */ + public function setUrl(string $url): NodeData + { + $this->url = $url; + return $this; + } + + /** + * @param string $updateUrl + * + * @return NodeData + */ + public function setUpdateUrl(string $updateUrl): NodeData + { + $this->updateUrl = $updateUrl; + return $this; + } + + /** + * @param int $generation + * + * @return NodeData + */ + public function setGeneration(int $generation): NodeData + { + $this->generation = $generation; + return $this; + } + + /** + * @return string + */ + public function getName(): string + { + return $this->name; + } + + /** + * @param string $name + * + * @return NodeData + */ + public function setName(string $name): NodeData + { + $this->name = $name; + return $this; + } + + /** + * @param bool $isNameRtl + * + * @return NodeData + */ + public function setIsNameRtl(bool $isNameRtl): NodeData + { + $this->isNameRtl = $isNameRtl; + return $this; + } + + /** + * @param string[] $firstNames + * + * @return NodeData + */ + public function setFirstNames(array $firstNames): NodeData + { + $this->firstNames = $firstNames; + return $this; + } + + /** + * @param string[] $lastNames + * + * @return NodeData + */ + public function setLastNames(array $lastNames): NodeData + { + $this->lastNames = $lastNames; + return $this; + } + + /** + * @param string $preferredName + * + * @return NodeData + */ + public function setPreferredName(string $preferredName): NodeData + { + $this->preferredName = $preferredName; + return $this; + } + + /** + * @param string $alternativeName + * + * @return NodeData + */ + public function setAlternativeName(string $alternativeName): NodeData + { + $this->alternativeName = $alternativeName; + return $this; + } + + /** + * @param bool $isAltRtl + * + * @return NodeData + */ + public function setIsAltRtl(bool $isAltRtl): NodeData + { + $this->isAltRtl = $isAltRtl; + return $this; + } + + /** + * @param string $thumbnail + * + * @return NodeData + */ + public function setThumbnail(string $thumbnail): NodeData + { + $this->thumbnail = $thumbnail; + return $this; + } + + /** + * @param string $sex + * + * @return NodeData + */ + public function setSex(string $sex): NodeData + { + $this->sex = $sex; + return $this; + } + + /** + * @param string $birth + * + * @return NodeData + */ + public function setBirth(string $birth): NodeData + { + $this->birth = $birth; + return $this; + } + + /** + * @param string $death + * + * @return NodeData + */ + public function setDeath(string $death): NodeData + { + $this->death = $death; + return $this; + } + + /** + * @param string $timespan + * + * @return NodeData + */ + public function setTimespan(string $timespan): NodeData + { + $this->timespan = $timespan; + return $this; + } + + /** + * @return null|Individual + */ + public function getIndividual(): ?Individual + { + return $this->individual; + } + + /** + * @param null|Individual $individual + * + * @return NodeData + */ + public function setIndividual(?Individual $individual): NodeData + { + $this->individual = $individual; + return $this; + } + + /** + * Returns the relevant data as an array. + * + * @return array + */ + public function jsonSerialize(): array + { + return [ + 'id' => $this->id, + 'xref' => $this->xref, + 'url' => $this->url, + 'updateUrl' => $this->updateUrl, + 'generation' => $this->generation, + 'name' => $this->name, + 'isNameRtl' => $this->isNameRtl, + 'firstNames' => $this->firstNames, + 'lastNames' => $this->lastNames, + 'preferredName' => $this->preferredName, + 'alternativeName' => $this->alternativeName, + 'isAltRtl' => $this->isAltRtl, + 'thumbnail' => $this->thumbnail, + 'sex' => $this->sex, + 'birth' => $this->birth, + 'death' => $this->death, + 'timespan' => $this->timespan, + ]; + } +} diff --git a/src/Module.php b/src/Module.php index 0d9aedb..0ce34d3 100644 --- a/src/Module.php +++ b/src/Module.php @@ -4,7 +4,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ declare(strict_types=1); @@ -13,7 +13,6 @@ use Fig\Http\Message\RequestMethodInterface; use Fisharebest\Webtrees\Auth; -use Fisharebest\Webtrees\Family; use Fisharebest\Webtrees\I18N; use Fisharebest\Webtrees\Individual; use Fisharebest\Webtrees\Module\ModuleChartInterface; @@ -21,10 +20,11 @@ use Fisharebest\Webtrees\Module\ModuleThemeInterface; use Fisharebest\Webtrees\Module\PedigreeChartModule; use Fisharebest\Webtrees\Registry; +use Fisharebest\Webtrees\Services\ChartService; use Fisharebest\Webtrees\Validator; use Fisharebest\Webtrees\View; use JsonException; -use MagicSunday\Webtrees\PedigreeChart\Traits\IndividualTrait; +use MagicSunday\Webtrees\PedigreeChart\Facade\DataFacade; use MagicSunday\Webtrees\PedigreeChart\Traits\ModuleChartTrait; use MagicSunday\Webtrees\PedigreeChart\Traits\ModuleCustomTrait; use Psr\Http\Message\ResponseInterface; @@ -41,7 +41,6 @@ class Module extends PedigreeChartModule implements ModuleCustomInterface { use ModuleCustomTrait; use ModuleChartTrait; - use IndividualTrait; private const ROUTE_DEFAULT = 'webtrees-pedigree-chart'; private const ROUTE_DEFAULT_URL = '/tree/{tree}/webtrees-pedigree-chart/{xref}'; @@ -78,6 +77,26 @@ class Module extends PedigreeChartModule implements ModuleCustomInterface */ private Configuration $configuration; + /** + * @var DataFacade + */ + private DataFacade $dataFacade; + + /** + * Constructor. + * + * @param ChartService $chartService + * @param DataFacade $dataFacade + */ + public function __construct( + ChartService $chartService, + DataFacade $dataFacade + ) { + parent::__construct($chartService); + + $this->dataFacade = $dataFacade; + } + /** * Initialization. */ @@ -113,7 +132,7 @@ public function description(): string } /** - * Where does this module store its resources + * Where does this module store its resources? * * @return string */ @@ -167,11 +186,16 @@ public function handle(ServerRequestInterface $request): ResponseInterface if ($ajax) { $this->layout = $this->name() . '::layouts/ajax'; + $this->dataFacade + ->setModule($this) + ->setConfiguration($this->configuration) + ->setRoute(self::ROUTE_DEFAULT); + return $this->viewResponse( $this->name() . '::modules/pedigree-chart/chart', [ 'id' => uniqid(), - 'data' => $this->buildJsonTree($individual), + 'data' => $this->dataFacade->createTreeStructure($individual), 'configuration' => $this->configuration, 'chartParams' => $this->getChartParameters(), 'exportStylesheets' => $this->getExportStylesheets(), @@ -229,47 +253,6 @@ private function getChartParameters(): array ]; } - /** - * Recursively build the data array of the individual ancestors. - * - * @param null|Individual $individual The start person - * @param int $generation The current generation - * - * @return mixed[] - */ - private function buildJsonTree(?Individual $individual, int $generation = 1): array - { - // Maximum generation reached - if (($individual === null) || ($generation > $this->configuration->getGenerations())) { - return []; - } - - /** @var array> $data */ - $data = $this->getIndividualData($individual, $generation); - - /** @var null|Family $family */ - $family = $individual->childFamilies()->first(); - - if ($family === null) { - return $data; - } - - // Recursively call the method for the parents of the individual - $fatherTree = $this->buildJsonTree($family->husband(), $generation + 1); - $motherTree = $this->buildJsonTree($family->wife(), $generation + 1); - - // Add array of child nodes - if ($fatherTree) { - $data['parents'][] = $fatherTree; - } - - if ($motherTree) { - $data['parents'][] = $motherTree; - } - - return $data; - } - /** * * @param Individual $individual @@ -290,43 +273,6 @@ private function getAjaxRoute(Individual $individual, string $xref): string ); } - /** - * Get the raw update URL. The "xref" parameter must be the last one as the URL gets appended - * with the clicked individual id in order to load the required chart data. - * - * @param Individual $individual - * - * @return string - */ - private function getUpdateRoute(Individual $individual): string - { - return $this->chartUrl( - $individual, - [ - 'generations' => $this->configuration->getGenerations(), - 'layout' => $this->configuration->getLayout(), - ] - ); - } - - /** - * Returns whether the given text is in RTL style or not. - * - * @param string[] $text The text to check - * - * @return bool - */ - private function isRtl(array $text): bool - { - foreach ($text as $entry) { - if (I18N::scriptDirection(I18N::textScript($entry)) === 'rtl') { - return true; - } - } - - return false; - } - /** * Returns a list of used stylesheets with this module. * diff --git a/src/Processor/DateProcessor.php b/src/Processor/DateProcessor.php new file mode 100644 index 0000000..466a0dc --- /dev/null +++ b/src/Processor/DateProcessor.php @@ -0,0 +1,180 @@ + + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-module-base/ + */ +class DateProcessor +{ + /** + * The individual. + * + * @var Individual + */ + private Individual $individual; + + /** + * The birthdate of the individual. + * + * @var Date + */ + private Date $birthDate; + + /** + * The death date of the individual. + * + * @var Date + */ + private Date $deathDate; + + /** + * Constructor. + * + * @param Individual $individual The individual to process + */ + public function __construct(Individual $individual) + { + $this->individual = $individual; + $this->birthDate = $this->individual->getBirthDate(); + $this->deathDate = $this->individual->getDeathDate(); + } + + /** + * Removes HTML tags and converts/decodes HTML entities to their corresponding characters. + * + * @param string $value The value to decode + * + * @return string + */ + private function decodeValue(string $value): string + { + return html_entity_decode(strip_tags($value), ENT_QUOTES, 'UTF-8'); + } + + /** + * Get the year of birth. + * + * @return int + */ + public function getBirthYear(): int + { + return $this->birthDate->minimumDate()->year(); + } + + /** + * Get the year of death. + * + * @return int + */ + public function getDeathYear(): int + { + return $this->deathDate->minimumDate()->year(); + } + + /** + * Returns the formatted birthdate without HTML tags. + * + * @return string + */ + public function getBirthDate(): string + { + return $this->decodeValue( + $this->birthDate->display() + ); + } + + /** + * Returns the formatted death date without HTML tags. + * + * @return string + */ + public function getDeathDate(): string + { + return $this->decodeValue( + $this->deathDate->display() + ); + } + + /** + * Create the timespan label. + * + * @return string + */ + public function getLifetimeDescription(): string + { + if ($this->birthDate->isOK() && $this->deathDate->isOK()) { + return $this->getBirthYear() . '-' . $this->getDeathYear(); + } + + if ($this->birthDate->isOK()) { + return I18N::translate('Born: %s', (string) $this->getBirthYear()); + } + + if ($this->deathDate->isOK()) { + return I18N::translate('Died: %s', (string) $this->getDeathYear()); + } + + if ($this->individual->isDead()) { + return I18N::translate('Deceased'); + } + + return ''; + } + + /** + * Returns the marriage date of the individual. + * + * @return string + */ + public function getMarriageDate(): string + { + /** @var null|Family $family */ + $family = $this->individual->spouseFamilies()->first(); + + if ($family !== null) { + return $this->decodeValue( + $family->getMarriageDate()->display() + ); + } + + return ''; + } + + /** + * Returns the marriage date of the parents. + * + * @return string + */ + public function getMarriageDateOfParents(): string + { + /** @var null|Family $family */ + $family = $this->individual->childFamilies()->first(); + + if ($family !== null) { + return $this->decodeValue( + $family->getMarriageDate()->display() + ); + } + + return ''; + } +} diff --git a/src/Processor/ImageProcessor.php b/src/Processor/ImageProcessor.php new file mode 100644 index 0000000..d1299cb --- /dev/null +++ b/src/Processor/ImageProcessor.php @@ -0,0 +1,92 @@ + + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-module-base/ + */ +class ImageProcessor +{ + /** + * The module. + * + * @var ModuleCustomInterface + */ + private ModuleCustomInterface $module; + + /** + * The individual. + * + * @var Individual + */ + private Individual $individual; + + /** + * Constructor. + * + * @param ModuleCustomInterface $module The module + * @param Individual $individual The individual to process + */ + public function __construct(ModuleCustomInterface $module, Individual $individual) + { + $this->module = $module; + $this->individual = $individual; + } + + /** + * Returns the URL of a person's highlight image. + * + * @param int $width The request maximum width of the image + * @param int $height The request maximum height of the image + * @param bool $returnSilhouettes Set to TRUE to return silhouette images if this is + * also enabled in the configuration + * + * @return string + */ + public function getHighlightImageUrl( + int $width = 250, + int $height = 250, + bool $returnSilhouettes = true + ): string { + if ( + $this->individual->canShow() + && ($this->individual->tree()->getPreference('SHOW_HIGHLIGHT_IMAGES') !== '') + ) { + $mediaFile = $this->individual->findHighlightedMediaFile(); + + if ($mediaFile !== null) { + return $mediaFile->imageUrl($width, $height, 'contain'); + } + + if ( + $returnSilhouettes + && ($this->individual->tree()->getPreference('USE_SILHOUETTE') !== '') + ) { + return $this->module->assetUrl( + sprintf( + 'images/silhouette-%s.svg', + $this->individual->sex() + ) + ); + } + } + + return ''; + } +} diff --git a/src/Processor/NameProcessor.php b/src/Processor/NameProcessor.php new file mode 100644 index 0000000..3107cce --- /dev/null +++ b/src/Processor/NameProcessor.php @@ -0,0 +1,255 @@ + + * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 + * @link https://github.com/magicsunday/webtrees-module-base/ + */ +class NameProcessor +{ + /** + * The full name identifier with name placeholders. + */ + private const FULL_NAME_WITH_PLACEHOLDERS = 'fullNN'; + + /** + * The full name identifier. + */ + private const FULL_NAME = 'full'; + + /** + * The XPath identifier to extract the starred name part. + */ + private const XPATH_PREFERRED_NAME = '//span[@class="NAME"]//span[@class="starredname"]/text()'; + + /** + * The individual. + * + * @var Individual + */ + private Individual $individual; + + /** + * The individual's primary name array. + * + * @var string[] + */ + private array $primaryName; + + /** + * The DOM xpath processor. + * + * @var DOMXPath + */ + private DOMXPath $xPath; + + /** + * Constructor. + * + * @param Individual $individual The individual to process + * @param null|Individual $spouse + * @param bool $useMarriedName TRUE to return the married name instead of the primary one + */ + public function __construct( + Individual $individual, + Individual $spouse = null, + bool $useMarriedName = false + ) { + $this->individual = $individual; + $this->primaryName = $this->extractPrimaryName($spouse, $useMarriedName); + + // The formatted name of the individual (containing HTML) is the input to the xpath processor + $this->xPath = $this->getDomXPathInstance($this->primaryName[self::FULL_NAME]); + } + + /** + * Returns the DOMXPath instance. + * + * @param string $input The input used as xpath base + * + * @return DOMXPath + */ + private function getDomXPathInstance(string $input): DOMXPath + { + $document = new DOMDocument(); + $document->loadHTML($this->convertToHtmlEntities($input)); + + return new DOMXPath($document); + } + + /** + * Extracts the primary name from the individual. + * + * @param null|Individual $spouse + * @param bool $useMarriedName TRUE to return the married name instead of the primary one + * + * @return array + */ + private function extractPrimaryName( + Individual $spouse = null, + bool $useMarriedName = false + ): array { + $individualNames = $this->individual->getAllNames(); + + if ($useMarriedName !== false) { + foreach ($individualNames as $individualName) { + if ($spouse !== null) { + foreach ($spouse->getAllNames() as $spouseName) { + if ( + ($individualName['type'] === '_MARNM') + && ($individualName['surn'] === $spouseName['surn']) + ) { + return $individualName; + } + } + } elseif ($individualName['type'] === '_MARNM') { + return $individualName; + } + } + } + + return $individualNames[$this->individual->getPrimaryName()]; + } + + /** + * Returns the UTF-8 chars converted to HTML entities. + * + * @param string $input The input to encode + * + * @return string + */ + private function convertToHtmlEntities(string $input): string + { + return mb_encode_numericentity($input, [0x80, 0xfffffff, 0, 0xfffffff], 'UTF-8'); + } + + /** + * Replace name placeholders. + * + * @param string $value + * + * @return string + */ + private function replacePlaceholders(string $value): string + { + return trim( + str_replace( + [ + Individual::NOMEN_NESCIO, + Individual::PRAENOMEN_NESCIO, + ], + '…', + $value + ) + ); + } + + /** + * Splits a name into an array, removing all name placeholders. + * + * @param string $name + * + * @return string[] + */ + private function splitAndCleanName(string $name): array + { + return array_values( + array_filter( + explode( + ' ', + $this->replacePlaceholders($name) + ) + ) + ); + } + + /** + * Returns the full name of the individual without formatting of the individual parts of the name. + * All placeholders were removed as we do not need them in this module. + * + * @return string + */ + public function getFullName(): string + { + // The name of the person without formatting of the individual parts of the name. + // Remove placeholders as we do not need them in this module + return $this->replacePlaceholders($this->primaryName[self::FULL_NAME_WITH_PLACEHOLDERS]); + } + + /** + * Returns all assigned first names of the individual. + * + * @return string[] + */ + public function getFirstNames(): array + { + return $this->splitAndCleanName($this->primaryName['givn']); + } + + /** + * Returns all assigned last names of the individual. + * + * @return string[] + */ + public function getLastNames(): array + { + return $this->splitAndCleanName($this->primaryName['surn']); + } + + /** + * Returns the preferred name of the individual. + * + * @return string + */ + public function getPreferredName(): string + { + $nodeList = $this->xPath->query(self::XPATH_PREFERRED_NAME); + + if (($nodeList !== false) && ($nodeList->length > 0)) { + $nodeItem = $nodeList->item(0); + + return ($nodeItem !== null) ? ($nodeItem->nodeValue ?? '') : ''; + } + + return ''; + } + + /** + * Returns the alternative name of the individual. + * + * @param Individual $individual + * + * @return string + */ + public function getAlternateName(Individual $individual): string + { + if ( + $individual->canShowName() + && ($individual->getPrimaryName() !== $individual->getSecondaryName()) + ) { + $allNames = $individual->getAllNames(); + $alternativeName = $allNames[$individual->getSecondaryName()][self::FULL_NAME_WITH_PLACEHOLDERS]; + + return $this->replacePlaceholders($alternativeName); + } + + return ''; + } +} diff --git a/src/Traits/IndividualTrait.php b/src/Traits/IndividualTrait.php deleted file mode 100644 index 2660226..0000000 --- a/src/Traits/IndividualTrait.php +++ /dev/null @@ -1,337 +0,0 @@ - - * @license https://opensource.org/licenses/GPL-3.0 GNU General Public License v3.0 - * @link https://github.com/magicsunday/webtrees-pedigree-chart/ - */ -trait IndividualTrait -{ - /** - * The XPath identifier to extract the first name parts. - * - * @var string - */ - private string $xpathFirstNames - = '//span[@class="NAME"]//text()[parent::*[not(@class="wt-nickname")]][following::span[@class="SURN"]]'; - - /** - * The XPath identifier to extract the last name parts (surname + surname suffix). - * - * @var string - */ - private string $xpathLastNames - = '//span[@class="NAME"]//span[@class="SURN"]/text()|//span[@class="SURN"]/following::text()'; - - /** - * The XPath identifier to extract the nickname part. - * - * @var string - */ - private string $xpathNickname = '//span[@class="NAME"]//q[@class="wt-nickname"]/text()'; - - /** - * The XPath identifier to extract the starred name part. - * - * @var string - */ - private string $xpathPreferredName = '//span[@class="NAME"]//span[@class="starredname"]/text()'; - - /** - * The XPath identifier to extract the alternative name parts. - * - * @var string - */ - private string $xpathAlternativeName = '//span[contains(attribute::class, "NAME")]'; - - /** - * Get the individual data required for display the chart. - * - * @param Individual $individual The current individual - * @param int $generation The generation the person belongs to - * - * @return array|bool|int|string> - */ - private function getIndividualData(Individual $individual, int $generation): array - { - $primaryName = $individual->getAllNames()[$individual->getPrimaryName()]; - - // The formatted name of the individual (containing HTML) - $full = $primaryName['full']; - - // Get xpath - $xpath = $this->getXPath($full); - - // The name of the person without formatting of the individual parts of the name. - // Remove placeholders as we do not need them in this module - $fullNN = str_replace( - [ - Individual::NOMEN_NESCIO, - Individual::PRAENOMEN_NESCIO, - ], - '', - $primaryName['fullNN'] - ); - - // Extract name parts (Do not change processing order!) - $preferredName = $this->getPreferredName($xpath); - $lastNames = $this->getLastNames($xpath); - $firstNames = $this->getFirstNames($xpath); - $alternativeNames = $this->getAlternateNames($individual); - - return [ - 'id' => 0, - 'xref' => $individual->xref(), - 'url' => $individual->url(), - 'updateUrl' => $this->getUpdateRoute($individual), - 'generation' => $generation, - 'name' => $fullNN, - 'firstNames' => $firstNames, - 'lastNames' => $lastNames, - 'preferredName' => $preferredName, - 'alternativeNames' => $alternativeNames, - 'isAltRtl' => $this->isRtl($alternativeNames), - 'thumbnail' => $this->getIndividualImage($individual), - 'sex' => $individual->sex(), - 'birth' => $this->decodeValue($individual->getBirthDate()->display()), - 'death' => $this->decodeValue($individual->getDeathDate()->display()), - 'timespan' => $this->getLifetimeDescription($individual), - ]; - } - - /** - * Returns the UTF-8 chars converted to HTML entities. - * - * @param string $input The input to encode - * - * @return string - */ - private function convertToHtmlEntities(string $input): string - { - return mb_encode_numericentity($input, [0x80, 0xfffffff, 0, 0xfffffff], 'UTF-8'); - } - - /** - * Returns the DOMXPath instance. - * - * @param string $fullName The individuals full name (containing HTML) - * - * @return DOMXPath - */ - private function getXPath(string $fullName): DOMXPath - { - $document = new DOMDocument(); - $document->loadHTML($this->convertToHtmlEntities($fullName)); - - return new DOMXPath($document); - } - - /** - * Create the timespan label. - * - * @param Individual $individual The current individual - * - * @return string - */ - private function getLifetimeDescription(Individual $individual): string - { - if ($individual->getBirthDate()->isOK() && $individual->getDeathDate()->isOK()) { - return $this->getBirthYear($individual) . '-' . $this->getDeathYear($individual); - } - - if ($individual->getBirthDate()->isOK()) { - return I18N::translate('Born: %s', $this->getBirthYear($individual)); - } - - if ($individual->getDeathDate()->isOK()) { - return I18N::translate('Died: %s', $this->getDeathYear($individual)); - } - - if ($individual->isDead()) { - return I18N::translate('Deceased'); - } - - return ''; - } - - /** - * Get the year of birth. - * - * @param Individual $individual The current individual - * - * @return string - */ - private function getBirthYear(Individual $individual): string - { - return $this->decodeValue( - $individual->getBirthDate()->minimumDate()->format('%Y') - ); - } - - /** - * Get the year of death. - * - * @param Individual $individual The current individual - * - * @return string - */ - private function getDeathYear(Individual $individual): string - { - return $this->decodeValue( - $individual->getDeathDate()->minimumDate()->format('%Y') - ); - } - - /** - * Removes HTML tags and converts/decodes HTML entities to their corresponding characters. - * - * @param string $value - * - * @return string - */ - private function decodeValue(string $value): string - { - return html_entity_decode(strip_tags($value), ENT_QUOTES, 'UTF-8'); - } - - /** - * Returns all first names from the given full name. - * - * @param DOMXPath $xpath The DOMXPath instance used to parse for the preferred name. - * - * @return string[] - */ - public function getFirstNames(DOMXPath $xpath): array - { - $nodeList = $xpath->query($this->xpathFirstNames); - $firstNames = []; - - if ($nodeList !== false) { - /** @var DOMNode $node */ - foreach ($nodeList as $node) { - $firstNames[] = $node->nodeValue !== null ? trim($node->nodeValue) : ''; - } - } - - $firstNames = explode(' ', implode(' ', $firstNames)); - - return array_values(array_filter($firstNames)); - } - - /** - * Returns all last names from the given full name. - * - * @param DOMXPath $xpath The DOMXPath instance used to parse for the preferred name. - * - * @return string[] - */ - public function getLastNames(DOMXPath $xpath): array - { - $nodeList = $xpath->query($this->xpathLastNames); - $lastNames = []; - - if ($nodeList !== false) { - /** @var DOMNode $node */ - foreach ($nodeList as $node) { - $lastNames[] = $node->nodeValue !== null ? trim($node->nodeValue) : ''; - } - } - - // Concat to full last name (as SURN may contain a prefix and a separate suffix) - $lastNames = explode(' ', implode(' ', $lastNames)); - - return array_values(array_filter($lastNames)); - } - - /** - * Returns the preferred name from the given full name. - * - * @param DOMXPath $xpath The DOMXPath instance used to parse for the preferred name. - * - * @return string - */ - public function getPreferredName(DOMXPath $xpath): string - { - $nodeList = $xpath->query($this->xpathPreferredName); - - if (($nodeList !== false) && $nodeList->length) { - $nodeItem = $nodeList->item(0); - - return ($nodeItem !== null) ? ($nodeItem->nodeValue ?? '') : ''; - } - - return ''; - } - - /** - * Returns the preferred name from the given full name. - * - * @param Individual $individual - * - * @return string[] - */ - public function getAlternateNames(Individual $individual): array - { - $name = $individual->alternateName(); - - if ($name === null) { - return []; - } - - $xpath = $this->getXPath($name); - $nodeList = $xpath->query($this->xpathAlternativeName); - - if (($nodeList !== false) && $nodeList->length) { - $nodeItem = $nodeList->item(0); - $name = ($nodeItem !== null) ? ($nodeItem->nodeValue ?? '') : ''; - } - - return array_filter(explode(' ', $name)); - } - - /** - * Returns the URL of the highlight image of an individual. - * - * @param Individual $individual The current individual - * - * @return string - */ - private function getIndividualImage(Individual $individual): string - { - if ( - $individual->canShow() - && $individual->tree()->getPreference('SHOW_HIGHLIGHT_IMAGES') - ) { - $mediaFile = $individual->findHighlightedMediaFile(); - - if ($mediaFile !== null) { - return $mediaFile->imageUrl(250, 250, 'contain'); - } - - if ($individual->tree()->getPreference('USE_SILHOUETTE')) { - return $this->assetUrl(sprintf('images/silhouette-%s.svg', $individual->sex())); - } - } - - return ''; - } -} diff --git a/src/Traits/ModuleChartTrait.php b/src/Traits/ModuleChartTrait.php index d0483cf..7897116 100644 --- a/src/Traits/ModuleChartTrait.php +++ b/src/Traits/ModuleChartTrait.php @@ -4,7 +4,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ declare(strict_types=1); diff --git a/src/Traits/ModuleCustomTrait.php b/src/Traits/ModuleCustomTrait.php index 75aa9f6..3311a29 100644 --- a/src/Traits/ModuleCustomTrait.php +++ b/src/Traits/ModuleCustomTrait.php @@ -4,7 +4,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ declare(strict_types=1); diff --git a/test/MultiByteTest.php b/test/MultiByteTest.php index 211b383..b9385b7 100644 --- a/test/MultiByteTest.php +++ b/test/MultiByteTest.php @@ -4,7 +4,7 @@ * This file is part of the package magicsunday/webtrees-pedigree-chart. * * For the full copyright and license information, please read the - * LICENSE file that was distributed with this source code. + * LICENSE file distributed with this source code. */ declare(strict_types=1);