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