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