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