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