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