xref: /webtrees/resources/views/lists/families-table.phtml (revision aad3b6f1952da257322ab03fcdd15fbdd92f9eb3)
1<?php
2
3declare(strict_types=1);
4
5use Fisharebest\Webtrees\Carbon;
6use Fisharebest\Webtrees\Date;
7use Fisharebest\Webtrees\Gedcom;
8use Fisharebest\Webtrees\GedcomTag;
9use Fisharebest\Webtrees\I18N;
10use Fisharebest\Webtrees\Individual;
11use Fisharebest\Webtrees\View;
12use Ramsey\Uuid\Uuid;
13
14$table_id = 'table-fam-' . Uuid::uuid4()->toString(); // lists requires a unique ID in case there are multiple lists per page
15
16$today_jd          = Carbon::now()->julianDay();
17$hundred_years_ago = Carbon::now()->subYears(100)->julianDay();
18
19?>
20
21<?php View::push('javascript') ?>
22<script>
23$("#<?= e($table_id) ?> > .wt-table-family").dataTable({
24    processing: true,
25    retrieve: true,
26    columns: [
27        /* Given names       */ { type: "text" },
28        /* Surnames          */ { type: "text" },
29        /* Age               */ { type: "num" },
30        /* Given names       */ { type: "text" },
31        /* Surnames          */ { type: "text" },
32        /* Age               */ { type: "num" },
33        /* Marriage date     */ { type: "num" },
34        /* Anniversary       */ { type: "num" },
35        /* Marriage place    */ { type: "text" },
36        /* Children          */ { type: "num" },
37        /* Last change       */ { visible: <?= json_encode((bool) $tree->getPreference('SHOW_LAST_CHANGE')) ?> },
38        /* Filter marriage   */ { sortable: false },
39        /* Filter alive/dead */ { sortable: false },
40        /* Filter tree       */ { sortable: false }
41    ],
42    sorting: [
43        [1, "asc"]
44    ]
45});
46
47$("#<?= e($table_id) ?>")
48    /* Hide/show parents */
49    .on("click", "#btn-toggle-parents", function() {
50        $(".wt-individual-list-parents").slideToggle();
51    })
52    /* Hide/show statistics */
53    .on("click", "#btn-toggle-statistics", function() {
54        $("#family-charts-<?= e($table_id) ?>").slideToggle({
55            complete: function () {
56                // Trigger resize to redraw the chart
57                $('div[id^="google-chart-"]').resize();
58            }
59        });
60    })
61    /* Filter buttons in table header */
62    .on("click", "input[data-filter-column]", function() {
63        let checkbox = $(this);
64        let siblings = checkbox.parent().siblings();
65
66        // Deselect other options
67        siblings.children().prop("checked", false).removeAttr("checked");
68        siblings.removeClass('active');
69
70        // Apply (or clear) this filter
71        let checked = checkbox.prop("checked");
72        let filter  = checked ? checkbox.data("filter-value") : "";
73        let column  = $("#<?= e($table_id) ?> .wt-table-family").DataTable().column(checkbox.data("filter-column"));
74        column.search(filter).draw();
75    });
76</script>
77<?php View::endpush() ?>
78
79<?php
80$max_age = (int) $tree->getPreference('MAX_ALIVE_AGE');
81
82// init chart data
83$marr_by_age = [];
84for ($age = 0; $age <= $max_age; $age++) {
85    $marr_by_age[$age]['M'] = 0;
86    $marr_by_age[$age]['F'] = 0;
87    $marr_by_age[$age]['U'] = 0;
88}
89$birt_by_decade = [];
90$marr_by_decade = [];
91for ($year = 1400; $year < 2050; $year += 10) {
92    $birt_by_decade[$year]['M'] = 0;
93    $birt_by_decade[$year]['F'] = 0;
94    $birt_by_decade[$year]['U'] = 0;
95    $marr_by_decade[$year]['M'] = 0;
96    $marr_by_decade[$year]['F'] = 0;
97    $marr_by_decade[$year]['U'] = 0;
98}
99
100$birthData = [
101    [
102        [
103            'label' => I18N::translate('Century'),
104            'type'  => 'date',
105        ], [
106            'label' => I18N::translate('Males'),
107            'type'  => 'number',
108        ], [
109            'label' => I18N::translate('Females'),
110            'type'  => 'number',
111        ],
112    ]
113];
114
115$marriageData = [
116    [
117        [
118            'label' => I18N::translate('Century'),
119            'type'  => 'date',
120        ], [
121            'label' => I18N::translate('Males'),
122            'type'  => 'number',
123        ], [
124            'label' => I18N::translate('Females'),
125            'type'  => 'number',
126        ],
127    ]
128];
129
130$marriageAgeData = [
131    [
132        I18N::translate('Age'),
133        I18N::translate('Males'),
134        I18N::translate('Females'),
135        I18N::translate('Average age'),
136    ]
137];
138
139?>
140
141<div id="<?= e($table_id) ?>">
142    <table class="table table-bordered table-sm wt-table-family"
143        <?= view('lists/datatables-attributes') ?>
144    >
145        <thead>
146            <tr>
147                <th colspan="14">
148                    <div class="btn-toolbar d-flex justify-content-between mb-2">
149                        <div class="btn-group btn-group-toggle btn-group-sm" data-toggle="buttons">
150                            <label class="btn btn-outline-secondary btn-sm" title="' . I18N::translate('Show individuals who are alive or couples where both partners are alive.') ?>">
151                                <input type="checkbox" data-filter-column="12" data-filter-value="N">
152                                <?= I18N::translate('Both alive') ?>
153                            </label>
154                            <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show couples where only the female partner is dead.') ?>">
155                                <input type="checkbox" data-filter-column="12" data-filter-value="W">
156                                <?= I18N::translate('Widower') ?>
157                            </label>
158                            <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show couples where only the male partner is dead.') ?>">
159                                <input type="checkbox" data-filter-column="12" data-filter-value="H">
160                                <?= I18N::translate('Widow') ?>
161                            </label>
162                            <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show individuals who are dead or couples where both partners are dead.') ?>">
163                                <input type="checkbox" data-filter-column="12" data-filter-value="Y">
164                                <?= I18N::translate('Both dead') ?>
165                            </label>
166                        </div>
167
168                        <div class="btn-group btn-group-toggle btn-group-sm" data-toggle="buttons">
169                            <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show “roots” couples or individuals. These individuals may also be called “patriarchs”. They are individuals who have no parents recorded in the database.') ?>">
170                                <input type="checkbox" data-filter-column="13" data-filter-value="R">
171                                <?= I18N::translate('Roots') ?>
172                            </label>
173                            <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show “leaves” couples or individuals. These are individuals who are alive but have no children recorded in the database.') ?>">
174                                <input type="checkbox" data-filter-column="13" data-filter-value="L">
175                                <?= I18N::translate('Leaves') ?>
176                            </label>
177                        </div>
178
179                        <div class="btn-group btn-group-toggle btn-group-sm" data-toggle="buttons">
180                            <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show couples with an unknown marriage date.') ?>">
181                                <input type="checkbox" data-filter-column="11" data-filter-value="U">
182                                <?= I18N::translate('Not married') ?>
183                            </label>
184                            <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show couples who married more than 100 years ago.') ?>">
185                                <input type="checkbox" data-filter-column="11" data-filter-value="YES">
186                                <?= I18N::translate('Marriage') ?>&gt;100
187                            </label>
188                            <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show couples who married within the last 100 years.') ?>">
189                                <input type="checkbox" data-filter-column="11" data-filter-value="Y100">
190                                <?= I18N::translate('Marriage') ?>&lt;=100
191                            </label>
192                            <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show divorced couples.') ?>">
193                                <input type="checkbox" data-filter-column="11" data-filter-value="D">
194                                <?= I18N::translate('Divorce') ?>
195                            </label>
196                            <label class="btn btn-outline-secondary" title="<?= I18N::translate('Show couples where either partner married more than once.') ?>">
197                                <input type="checkbox" data-filter-column="11" data-filter-value="M">
198                                <?= I18N::translate('Multiple marriages') ?>
199                            </label>
200                        </div>
201                    </div>
202                </th>
203            </tr>
204            <tr>
205                <th><?= I18N::translate('Given names') ?></th>
206                <th><?= I18N::translate('Surname') ?></th>
207                <th><?= I18N::translate('Age') ?></th>
208                <th><?= I18N::translate('Given names') ?></th>
209                <th><?= I18N::translate('Surname') ?></th>
210                <th><?= I18N::translate('Age') ?></th>
211                <th><?= I18N::translate('Marriage') ?></th>
212                <th>
213                    <span title="<?= I18N::translate('Anniversary') ?>">
214                        <?= view('icons/anniversary') ?>
215                    </span>
216                </th>
217                <th><?= I18N::translate('Place') ?></th>
218                <th><i class="icon-children" title="<?= I18N::translate('Children') ?>"></i></th>
219                <th><?= I18N::translate('Last change') ?></th>
220                <th hidden></th>
221                <th hidden></th>
222                <th hidden></th>
223            </tr>
224        </thead>
225
226        <tbody>
227        <?php foreach ($families as $family) : ?>
228            <?php $husb = $family->husband() ?? new Individual('H', '0 @H@ INDI', null, $family->tree()) ?>
229            <?php $wife = $family->wife() ?? new Individual('W', '0 @W@ INDI', null, $family->tree()) ?>
230
231            <tr class="<?= $family->isPendingDeletion() ? 'wt-old' : ($family->isPendingAddition() ? 'wt-new' : '') ?>">
232                <!-- Husband name -->
233                <td colspan="2" data-sort="<?= e(str_replace([',', '@P.N.', '@N.N.'], 'AAAA', implode(',', array_reverse(explode(',', $husb->sortName()))))) ?>">
234                    <?php foreach ($husb->getAllNames() as $num => $name) : ?>
235                        <?php if ($name['type'] !== '_MARNM' || $num == $husb->getPrimaryName()) : ?>
236                        <a title="<?= $name['type'] === 'NAME' ? '' : strip_tags(GedcomTag::getLabel($name['type'], $husb)) ?>" href="<?= e($family->url()) ?>" class="<?= $num === $husb->getPrimaryName() ? 'name2' : '' ?>">
237                            <?= $name['full'] ?>
238                        </a>
239                            <?php if ($num === $husb->getPrimaryName()) : ?>
240                                <small><?= view('icons/sex', ['sex' => $husb->sex()]) ?></small>
241                            <?php endif ?>
242                        <br>
243                        <?php endif ?>
244                    <?php endforeach ?>
245                    <?= view('lists/individual-table-parents', ['individual' => $husb]) ?>
246                </td>
247
248                <td hidden data-sort="<?= e(str_replace([',', '@P.N.', '@N.N.'], 'AAAA', $husb->sortName())) ?>"></td>
249
250                <!-- Husband age -->
251                <?php
252                $mdate = $family->getMarriageDate();
253                $hdate = $husb->getBirthDate();
254                if ($hdate->isOK() && $mdate->isOK()) {
255                    if ($hdate->gregorianYear() >= 1550 && $hdate->gregorianYear() < 2030) {
256                        ++$birt_by_decade[(int) ($hdate->gregorianYear() / 10) * 10][$husb->sex()];
257                    }
258                    $hage = Date::getAgeYears($hdate, $mdate);
259                    if ($hage >= 0 && $hage <= $max_age) {
260                        ++$marr_by_age[$hage][$husb->sex()];
261                    }
262                }
263                ?>
264                <td class="text-center" data-sort="<?= Date::getAgeDays($hdate, $mdate) ?>">
265                    <?= Date::getAge($hdate, $mdate) ?>
266                </td>
267
268                <!-- Wife name -->
269                <td colspan="2" data-sort="<?= e(str_replace([',', '@P.N.', '@N.N.'], 'AAAA', implode(',', array_reverse(explode(',', $wife->sortName()))))) ?>">
270                    <?php foreach ($wife->getAllNames() as $num => $name) : ?>
271                        <?php if ($name['type'] !== '_MARNM' || $num == $wife->getPrimaryName()) : ?>
272                            <a title="<?= $name['type'] === 'NAME' ? '' : strip_tags(GedcomTag::getLabel($name['type'], $wife)) ?>" href="<?= e($family->url()) ?>" class="<?= $num === $wife->getPrimaryName() ? 'name2' : '' ?>">
273                                <?= $name['full'] ?>
274                            </a>
275                            <?php if ($num === $wife->getPrimaryName()) : ?>
276                                <small><?= view('icons/sex', ['sex' => $wife->sex()]) ?></small>
277                            <?php endif ?>
278                            <br>
279                        <?php endif ?>
280                    <?php endforeach ?>
281                    <?= view('lists/individual-table-parents', ['individual' => $wife]) ?>
282                </td>
283
284                <td hidden data-sort="<?= e(str_replace([',', '@P.N.', '@N.N.'], 'AAAA', $wife->sortName())) ?>"></td>
285
286                <!-- Wife age -->
287                <?php
288                $wdate = $wife->getBirthDate();
289                if ($wdate->isOK() && $mdate->isOK()) {
290                    if ($wdate->gregorianYear() >= 1550 && $wdate->gregorianYear() < 2030) {
291                        ++$birt_by_decade[(int) ($wdate->gregorianYear() / 10) * 10][$wife->sex()];
292                    }
293                    $wage = Date::getAgeYears($wdate, $mdate);
294                    if ($wage >= 0 && $wage <= $max_age) {
295                        ++$marr_by_age[$wage][$wife->sex()];
296                    }
297                }
298                ?>
299
300                <td class="text-center" data-sort="<?= Date::getAgeDays($wdate, $mdate) ?>">
301                    <?= Date::getAge($wdate, $mdate) ?>
302                </td>
303
304                <!-- Marriage date -->
305                <td data-sort="<?= $family->getMarriageDate()->julianDay() ?>">
306                    <?php if ($marriage_dates = $family->getAllMarriageDates()) : ?>
307                        <?php foreach ($marriage_dates as $n => $marriage_date) : ?>
308                            <div><?= $marriage_date->display(true) ?></div>
309                        <?php endforeach ?>
310                        <?php if ($marriage_dates[0]->gregorianYear() >= 1550 && $marriage_dates[0]->gregorianYear() < 2030) : ?>
311                            <?php
312                                ++$marr_by_decade[(int) ($marriage_dates[0]->gregorianYear() / 10) * 10][$husb->sex()];
313                                ++$marr_by_decade[(int) ($marriage_dates[0]->gregorianYear() / 10) * 10][$wife->sex()];
314                            ?>
315                        <?php endif ?>
316                    <?php elseif ($family->facts(['_NMR'])->isNotEmpty()) : ?>
317                        <?= I18N::translate('no') ?>
318                    <?php elseif ($family->facts(['MARR'])->isNotEmpty()) : ?>
319                            <?= I18N::translate('yes') ?>
320                    <?php endif ?>
321                </td>
322
323                <!-- Marriage anniversary -->
324                <td class="text-center" data-sort="<?= - $family->getMarriageDate()->julianDay() ?>">
325                    <?= Date::getAge($family->getMarriageDate(), null) ?>
326                </td>
327
328                <!-- Marriage place -->
329                <td>
330                    <?php foreach ($family->getAllMarriagePlaces() as $n => $marriage_place) : ?>
331                        <?= $marriage_place->shortName(true) ?>
332                        <br>
333                    <?php endforeach ?>
334                </td>
335
336                <!-- Number of children -->
337                <td class="text-center" data-sort="<?= $family->numberOfChildren() ?>">
338                    <?= I18N::number($family->numberOfChildren()) ?>
339                </td>
340
341                <!-- Last change -->
342                <td data-sort="<?= $family->lastChangeTimestamp()->unix() ?>">
343                    <?= view('components/datetime', ['timestamp' => $family->lastChangeTimestamp()]) ?>
344                </td>
345
346                <!-- Filter by marriage date -->
347                <td hidden>
348                    <?php if ($mdate->maximumJulianDay() > $hundred_years_ago && $mdate->maximumJulianDay() <= $today_jd) : ?>
349                        Y100
350                    <?php elseif ($family->facts(['MARR'])->isNotEmpty()) : ?>
351                        YES
352                    <?php else : ?>
353                        U
354                    <?php endif ?>
355                    <?php if ($family->facts(['DIV'])->isNotEmpty()) : ?>
356                        D
357                    <?php endif ?>
358                    <?php if (count($husb->spouseFamilies()) > 1 || count($wife->spouseFamilies()) > 1) : ?>
359                        M
360                    <?php endif ?>
361                </td>
362
363                <!-- Filter by alive/dead -->
364                <td hidden>
365                    <?php if ($husb->isDead() && $wife->isDead()) : ?>
366                        Y
367                    <?php endif ?>
368                    <?php if ($husb->isDead() && !$wife->isDead()) : ?>
369                        <?php if ($wife->sex() === 'F') : ?>
370                            H
371                        <?php endif ?>
372                        <?php if ($wife->sex() === 'M') : ?>
373                            W
374                        <?php endif ?>
375                    <?php endif ?>
376                    <?php if (!$husb->isDead() && $wife->isDead()) : ?>
377                        <?php if ($husb->sex() === 'M') : ?>
378                            W
379                        <?php endif ?>
380                        <?php if ($husb->sex() === 'F') : ?>
381                            H
382                        <?php endif ?>
383                    <?php endif ?>
384                    <?php if (!$husb->isDead() && !$wife->isDead()) : ?>
385                        N
386                    <?php endif ?>
387                </td>
388
389                <!-- Filter by roots/leaves -->
390                <td hidden>
391                    <?php if (!$husb->childFamilies() && !$wife->childFamilies()) : ?>
392                        R
393                    <?php elseif (!$husb->isDead() && !$wife->isDead() && $family->numberOfChildren() === 0) : ?>
394                        L
395                    <?php endif ?>
396                </td>
397            </tr>
398        <?php endforeach ?>
399        </tbody>
400
401        <tfoot>
402            <tr>
403                <th colspan="14">
404                    <div class="btn-group btn-group-sm">
405                        <button id="btn-toggle-parents" class="btn btn-outline-secondary" data-toggle="button" data-persist="show-parents">
406                            <?= I18N::translate('Show parents') ?>
407                        </button>
408                        <button id="btn-toggle-statistics" class="btn btn-outline-secondary" data-toggle="button" data-persist="show-statistics">
409                            <?= I18N::translate('Show statistics charts') ?>
410                        </button>
411                    </div>
412                </th>
413            </tr>
414        </tfoot>
415    </table>
416</div>
417
418<div id="family-charts-<?= e($table_id) ?>" style="display: none;">
419    <div class="mb-3">
420        <div class="card-deck">
421            <div class="col-lg-12 col-md-12 mb-3">
422                <div class="card m-0">
423                    <div class="card-header">
424                        <?= I18N::translate('Decade of birth') ?>
425                    </div><div class="card-body">
426                        <?php
427                        foreach ($birt_by_decade as $century => $values) {
428                            if (($values['M'] + $values['F']) > 0) {
429                                $birthData[] = [
430                                    [
431                                        'v' => 'Date(' . $century . ', 0, 1)',
432                                        'f' => $century,
433                                    ],
434                                    $values['M'],
435                                    $values['F'],
436                                ];
437                            }
438                        }
439                        ?>
440                        <?= view('lists/chart-by-decade', ['data' => $birthData, 'title' => I18N::translate('Decade of birth')]) ?>
441                    </div>
442                </div>
443            </div>
444        </div>
445        <div class="card-deck">
446            <div class="col-lg-12 col-md-12 mb-3">
447                <div class="card m-0">
448                    <div class="card-header">
449                        <?= I18N::translate('Decade of marriage') ?>
450                    </div><div class="card-body">
451                        <?php
452                        foreach ($marr_by_decade as $century => $values) {
453                            if (($values['M'] + $values['F']) > 0) {
454                                $marriageData[] = [
455                                    [
456                                        'v' => 'Date(' . $century . ', 0, 1)',
457                                        'f' => $century,
458                                    ],
459                                    $values['M'],
460                                    $values['F'],
461                                ];
462                            }
463                        }
464                        ?>
465                        <?= view('lists/chart-by-decade', ['data' => $marriageData, 'title' => I18N::translate('Decade of marriage')]) ?>
466                    </div>
467                </div>
468            </div>
469        </div>
470        <div class="card-deck">
471            <div class="col-lg-12 col-md-12 mb-3">
472                <div class="card m-0">
473                    <div class="card-header">
474                        <?= I18N::translate('Age in year of marriage') ?>
475                    </div>
476                    <div class="card-body">
477                        <?php
478                            $totalAge = 0;
479                            $totalSum = 0;
480                            $max      = 0;
481
482                        foreach ($marr_by_age as $age => $values) {
483                            if (($values['M'] + $values['F']) > 0) {
484                                if (($values['M'] + $values['F']) > $max) {
485                                    $max = $values['M'] + $values['F'];
486                                }
487
488                                $totalAge += $age * ($values['M'] + $values['F']);
489                                $totalSum += $values['M'] + $values['F'];
490
491                                $marriageAgeData[] = [
492                                    $age,
493                                    $values['M'],
494                                    $values['F'],
495                                    null,
496                                ];
497                            }
498                        }
499
500                        if ($totalSum > 0) {
501                            $marriageAgeData[] = [
502                                round($totalAge / $totalSum, 1),
503                                null,
504                                null,
505                                0,
506                            ];
507
508                            $marriageAgeData[] = [
509                                round($totalAge / $totalSum, 1),
510                                null,
511                                null,
512                                $max,
513                            ];
514                        }
515                        ?>
516                        <?= view('lists/chart-by-age', ['data' => $marriageAgeData, 'title' => I18N::translate('Age in year of marriage')]) ?>
517                    </div>
518                </div>
519            </div>
520        </div>
521    </div>
522</div>
523