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