xref: /webtrees/app/Statistics/Repository/IndividualRepository.php (revision b3679361640bf9d3bb5424fb15815c51cf0c1513)
1<?php
2
3/**
4 * webtrees: online genealogy
5 * Copyright (C) 2021 webtrees development team
6 * This program is free software: you can redistribute it and/or modify
7 * it under the terms of the GNU General Public License as published by
8 * the Free Software Foundation, either version 3 of the License, or
9 * (at your option) any later version.
10 * This program is distributed in the hope that it will be useful,
11 * but WITHOUT ANY WARRANTY; without even the implied warranty of
12 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
13 * GNU General Public License for more details.
14 * You should have received a copy of the GNU General Public License
15 * along with this program. If not, see <https://www.gnu.org/licenses/>.
16 */
17
18declare(strict_types=1);
19
20namespace Fisharebest\Webtrees\Statistics\Repository;
21
22use Fisharebest\Webtrees\Auth;
23use Fisharebest\Webtrees\Carbon;
24use Fisharebest\Webtrees\Functions\FunctionsPrintLists;
25use Fisharebest\Webtrees\Gedcom;
26use Fisharebest\Webtrees\GedcomRecord;
27use Fisharebest\Webtrees\I18N;
28use Fisharebest\Webtrees\Individual;
29use Fisharebest\Webtrees\Module\IndividualListModule;
30use Fisharebest\Webtrees\Module\ModuleInterface;
31use Fisharebest\Webtrees\Module\ModuleListInterface;
32use Fisharebest\Webtrees\Registry;
33use Fisharebest\Webtrees\Services\ModuleService;
34use Fisharebest\Webtrees\Statistics\Google\ChartAge;
35use Fisharebest\Webtrees\Statistics\Google\ChartBirth;
36use Fisharebest\Webtrees\Statistics\Google\ChartCommonGiven;
37use Fisharebest\Webtrees\Statistics\Google\ChartCommonSurname;
38use Fisharebest\Webtrees\Statistics\Google\ChartDeath;
39use Fisharebest\Webtrees\Statistics\Google\ChartFamilyWithSources;
40use Fisharebest\Webtrees\Statistics\Google\ChartIndividualWithSources;
41use Fisharebest\Webtrees\Statistics\Google\ChartMortality;
42use Fisharebest\Webtrees\Statistics\Google\ChartSex;
43use Fisharebest\Webtrees\Statistics\Repository\Interfaces\IndividualRepositoryInterface;
44use Fisharebest\Webtrees\Statistics\Service\CenturyService;
45use Fisharebest\Webtrees\Statistics\Service\ColorService;
46use Fisharebest\Webtrees\SurnameTradition;
47use Fisharebest\Webtrees\Tree;
48use Illuminate\Database\Capsule\Manager as DB;
49use Illuminate\Database\Query\Builder;
50use Illuminate\Database\Query\Expression;
51use Illuminate\Database\Query\JoinClause;
52
53use function app;
54use function array_key_exists;
55use function array_keys;
56use function array_reverse;
57use function array_shift;
58use function array_slice;
59use function array_walk;
60use function arsort;
61use function e;
62use function explode;
63use function implode;
64use function preg_match;
65use function uksort;
66use function view;
67
68/**
69 * A repository providing methods for individual related statistics.
70 */
71class IndividualRepository implements IndividualRepositoryInterface
72{
73    private CenturyService $century_service;
74
75    private ColorService $color_service;
76
77    private Tree $tree;
78
79    /**
80     * @param CenturyService $century_service
81     * @param ColorService $color_service
82     * @param Tree         $tree
83     */
84    public function __construct(CenturyService $century_service, ColorService $color_service, Tree $tree)
85    {
86        $this->century_service = $century_service;
87        $this->color_service   = $color_service;
88        $this->tree            = $tree;
89    }
90
91    /**
92     * Find common given names.
93     *
94     * @param string $sex
95     * @param string $type
96     * @param bool   $show_tot
97     * @param int    $threshold
98     * @param int    $maxtoshow
99     *
100     * @return string|array<int>
101     */
102    private function commonGivenQuery(string $sex, string $type, bool $show_tot, int $threshold, int $maxtoshow)
103    {
104        $query = DB::table('name')
105            ->join('individuals', static function (JoinClause $join): void {
106                $join
107                    ->on('i_file', '=', 'n_file')
108                    ->on('i_id', '=', 'n_id');
109            })
110            ->where('n_file', '=', $this->tree->id())
111            ->where('n_type', '<>', '_MARNM')
112            ->where('n_givn', '<>', Individual::PRAENOMEN_NESCIO)
113            ->where(new Expression('LENGTH(n_givn)'), '>', 1);
114
115        switch ($sex) {
116            case 'M':
117            case 'F':
118            case 'U':
119                $query->where('i_sex', '=', $sex);
120                break;
121
122            case 'B':
123            default:
124                $query->where('i_sex', '<>', 'U');
125                break;
126        }
127
128        $rows = $query
129            ->groupBy(['n_givn'])
130            ->select(['n_givn', new Expression('COUNT(distinct n_id) AS count')])
131            ->pluck('count', 'n_givn');
132
133        $nameList = [];
134
135        foreach ($rows as $n_givn => $count) {
136            // Split “John Thomas” into “John” and “Thomas” and count against both totals
137            foreach (explode(' ', (string) $n_givn) as $given) {
138                // Exclude initials and particles.
139                if (!preg_match('/^([A-Z]|[a-z]{1,3})$/', $given)) {
140                    if (array_key_exists($given, $nameList)) {
141                        $nameList[$given] += (int) $count;
142                    } else {
143                        $nameList[$given] = (int) $count;
144                    }
145                }
146            }
147        }
148        arsort($nameList);
149        $nameList = array_slice($nameList, 0, $maxtoshow);
150
151        foreach ($nameList as $given => $total) {
152            if ($total < $threshold) {
153                unset($nameList[$given]);
154            }
155        }
156
157        switch ($type) {
158            case 'chart':
159                return $nameList;
160
161            case 'table':
162                return view('lists/given-names-table', [
163                    'given_names' => $nameList,
164                ]);
165
166            case 'list':
167                return view('lists/given-names-list', [
168                    'given_names' => $nameList,
169                    'show_totals' => $show_tot,
170                ]);
171
172            case 'nolist':
173            default:
174                array_walk($nameList, static function (string &$value, string $key) use ($show_tot): void {
175                    if ($show_tot) {
176                        $value = '<bdi>' . e($key) . '</bdi> (' . I18N::number((int) $value) . ')';
177                    } else {
178                        $value = '<bdi>' . e($key) . '</bdi>';
179                    }
180                });
181
182                return implode(I18N::$list_separator, $nameList);
183        }
184    }
185
186    /**
187     * Find common give names.
188     *
189     * @param int $threshold
190     * @param int $maxtoshow
191     *
192     * @return string
193     */
194    public function commonGiven(int $threshold = 1, int $maxtoshow = 10): string
195    {
196        return $this->commonGivenQuery('B', 'nolist', false, $threshold, $maxtoshow);
197    }
198
199    /**
200     * Find common give names.
201     *
202     * @param int $threshold
203     * @param int $maxtoshow
204     *
205     * @return string
206     */
207    public function commonGivenTotals(int $threshold = 1, int $maxtoshow = 10): string
208    {
209        return $this->commonGivenQuery('B', 'nolist', true, $threshold, $maxtoshow);
210    }
211
212    /**
213     * Find common give names.
214     *
215     * @param int $threshold
216     * @param int $maxtoshow
217     *
218     * @return string
219     */
220    public function commonGivenList(int $threshold = 1, int $maxtoshow = 10): string
221    {
222        return $this->commonGivenQuery('B', 'list', false, $threshold, $maxtoshow);
223    }
224
225    /**
226     * Find common give names.
227     *
228     * @param int $threshold
229     * @param int $maxtoshow
230     *
231     * @return string
232     */
233    public function commonGivenListTotals(int $threshold = 1, int $maxtoshow = 10): string
234    {
235        return $this->commonGivenQuery('B', 'list', true, $threshold, $maxtoshow);
236    }
237
238    /**
239     * Find common give names.
240     *
241     * @param int $threshold
242     * @param int $maxtoshow
243     *
244     * @return string
245     */
246    public function commonGivenTable(int $threshold = 1, int $maxtoshow = 10): string
247    {
248        return $this->commonGivenQuery('B', 'table', false, $threshold, $maxtoshow);
249    }
250
251    /**
252     * Find common give names of females.
253     *
254     * @param int $threshold
255     * @param int $maxtoshow
256     *
257     * @return string
258     */
259    public function commonGivenFemale(int $threshold = 1, int $maxtoshow = 10): string
260    {
261        return $this->commonGivenQuery('F', 'nolist', false, $threshold, $maxtoshow);
262    }
263
264    /**
265     * Find common give names of females.
266     *
267     * @param int $threshold
268     * @param int $maxtoshow
269     *
270     * @return string
271     */
272    public function commonGivenFemaleTotals(int $threshold = 1, int $maxtoshow = 10): string
273    {
274        return $this->commonGivenQuery('F', 'nolist', true, $threshold, $maxtoshow);
275    }
276
277    /**
278     * Find common give names of females.
279     *
280     * @param int $threshold
281     * @param int $maxtoshow
282     *
283     * @return string
284     */
285    public function commonGivenFemaleList(int $threshold = 1, int $maxtoshow = 10): string
286    {
287        return $this->commonGivenQuery('F', 'list', false, $threshold, $maxtoshow);
288    }
289
290    /**
291     * Find common give names of females.
292     *
293     * @param int $threshold
294     * @param int $maxtoshow
295     *
296     * @return string
297     */
298    public function commonGivenFemaleListTotals(int $threshold = 1, int $maxtoshow = 10): string
299    {
300        return $this->commonGivenQuery('F', 'list', true, $threshold, $maxtoshow);
301    }
302
303    /**
304     * Find common give names of females.
305     *
306     * @param int $threshold
307     * @param int $maxtoshow
308     *
309     * @return string
310     */
311    public function commonGivenFemaleTable(int $threshold = 1, int $maxtoshow = 10): string
312    {
313        return $this->commonGivenQuery('F', 'table', false, $threshold, $maxtoshow);
314    }
315
316    /**
317     * Find common give names of males.
318     *
319     * @param int $threshold
320     * @param int $maxtoshow
321     *
322     * @return string
323     */
324    public function commonGivenMale(int $threshold = 1, int $maxtoshow = 10): string
325    {
326        return $this->commonGivenQuery('M', 'nolist', false, $threshold, $maxtoshow);
327    }
328
329    /**
330     * Find common give names of males.
331     *
332     * @param int $threshold
333     * @param int $maxtoshow
334     *
335     * @return string
336     */
337    public function commonGivenMaleTotals(int $threshold = 1, int $maxtoshow = 10): string
338    {
339        return $this->commonGivenQuery('M', 'nolist', true, $threshold, $maxtoshow);
340    }
341
342    /**
343     * Find common give names of males.
344     *
345     * @param int $threshold
346     * @param int $maxtoshow
347     *
348     * @return string
349     */
350    public function commonGivenMaleList(int $threshold = 1, int $maxtoshow = 10): string
351    {
352        return $this->commonGivenQuery('M', 'list', false, $threshold, $maxtoshow);
353    }
354
355    /**
356     * Find common give names of males.
357     *
358     * @param int $threshold
359     * @param int $maxtoshow
360     *
361     * @return string
362     */
363    public function commonGivenMaleListTotals(int $threshold = 1, int $maxtoshow = 10): string
364    {
365        return $this->commonGivenQuery('M', 'list', true, $threshold, $maxtoshow);
366    }
367
368    /**
369     * Find common give names of males.
370     *
371     * @param int $threshold
372     * @param int $maxtoshow
373     *
374     * @return string
375     */
376    public function commonGivenMaleTable(int $threshold = 1, int $maxtoshow = 10): string
377    {
378        return $this->commonGivenQuery('M', 'table', false, $threshold, $maxtoshow);
379    }
380
381    /**
382     * Find common give names of unknown sexes.
383     *
384     * @param int $threshold
385     * @param int $maxtoshow
386     *
387     * @return string
388     */
389    public function commonGivenUnknown(int $threshold = 1, int $maxtoshow = 10): string
390    {
391        return $this->commonGivenQuery('U', 'nolist', false, $threshold, $maxtoshow);
392    }
393
394    /**
395     * Find common give names of unknown sexes.
396     *
397     * @param int $threshold
398     * @param int $maxtoshow
399     *
400     * @return string
401     */
402    public function commonGivenUnknownTotals(int $threshold = 1, int $maxtoshow = 10): string
403    {
404        return $this->commonGivenQuery('U', 'nolist', true, $threshold, $maxtoshow);
405    }
406
407    /**
408     * Find common give names of unknown sexes.
409     *
410     * @param int $threshold
411     * @param int $maxtoshow
412     *
413     * @return string
414     */
415    public function commonGivenUnknownList(int $threshold = 1, int $maxtoshow = 10): string
416    {
417        return $this->commonGivenQuery('U', 'list', false, $threshold, $maxtoshow);
418    }
419
420    /**
421     * Find common give names of unknown sexes.
422     *
423     * @param int $threshold
424     * @param int $maxtoshow
425     *
426     * @return string
427     */
428    public function commonGivenUnknownListTotals(int $threshold = 1, int $maxtoshow = 10): string
429    {
430        return $this->commonGivenQuery('U', 'list', true, $threshold, $maxtoshow);
431    }
432
433    /**
434     * Find common give names of unknown sexes.
435     *
436     * @param int $threshold
437     * @param int $maxtoshow
438     *
439     * @return string
440     */
441    public function commonGivenUnknownTable(int $threshold = 1, int $maxtoshow = 10): string
442    {
443        return $this->commonGivenQuery('U', 'table', false, $threshold, $maxtoshow);
444    }
445
446    /**
447     * Count the number of distinct given names (or the number of occurences of specific given names).
448     *
449     * @param array<string> ...$params
450     *
451     * @return string
452     */
453    public function totalGivennames(...$params): string
454    {
455        $query = DB::table('name')
456            ->where('n_file', '=', $this->tree->id());
457
458        if ($params === []) {
459            // Count number of distinct given names.
460            $query
461                ->distinct()
462                ->where('n_givn', '<>', Individual::PRAENOMEN_NESCIO)
463                ->whereNotNull('n_givn');
464        } else {
465            // Count number of occurences of specific given names.
466            $query->whereIn('n_givn', $params);
467        }
468
469        $count = $query->count('n_givn');
470
471        return I18N::number($count);
472    }
473
474    /**
475     * Count the number of distinct surnames (or the number of occurrences of specific surnames).
476     *
477     * @param array<string> ...$params
478     *
479     * @return string
480     */
481    public function totalSurnames(...$params): string
482    {
483        $query = DB::table('name')
484            ->where('n_file', '=', $this->tree->id());
485
486        if ($params === []) {
487            // Count number of distinct surnames
488            $query->distinct()
489                ->whereNotNull('n_surn');
490        } else {
491            // Count number of occurences of specific surnames.
492            $query->whereIn('n_surn', $params);
493        }
494
495        $count = $query->count('n_surn');
496
497        return I18N::number($count);
498    }
499
500    /**
501     * @param int $number_of_surnames
502     * @param int $threshold
503     *
504     * @return array<array<int>>
505     */
506    private function topSurnames(int $number_of_surnames, int $threshold): array
507    {
508        // Use the count of base surnames.
509        $top_surnames = DB::table('name')
510            ->where('n_file', '=', $this->tree->id())
511            ->where('n_type', '<>', '_MARNM')
512            ->whereNotIn('n_surn', ['', Individual::NOMEN_NESCIO])
513            ->select(['n_surn'])
514            ->groupBy(['n_surn'])
515            ->orderByRaw('COUNT(n_surn) DESC')
516            ->orderBy(new Expression('COUNT(n_surn)'), 'DESC')
517            ->having(new Expression('COUNT(n_surn)'), '>=', $threshold)
518            ->take($number_of_surnames)
519            ->get()
520            ->pluck('n_surn')
521            ->all();
522
523        $surnames = [];
524
525        foreach ($top_surnames as $top_surname) {
526            $surnames[$top_surname] = DB::table('name')
527                ->where('n_file', '=', $this->tree->id())
528                ->where('n_type', '<>', '_MARNM')
529                ->where('n_surn', '=', $top_surname)
530                ->select(['n_surn', new Expression('COUNT(n_surn) AS count')])
531                ->groupBy(['n_surn'])
532                ->orderBy('n_surn')
533                ->get()
534                ->pluck('count', 'n_surn')
535                ->all();
536        }
537
538        return $surnames;
539    }
540
541    /**
542     * Find common surnames.
543     *
544     * @return string
545     */
546    public function getCommonSurname(): string
547    {
548        $top_surname = $this->topSurnames(1, 0);
549
550        return implode(', ', array_keys(array_shift($top_surname) ?? []));
551    }
552
553    /**
554     * Find common surnames.
555     *
556     * @param string $type
557     * @param bool   $show_tot
558     * @param int    $threshold
559     * @param int    $number_of_surnames
560     * @param string $sorting
561     *
562     * @return string
563     */
564    private function commonSurnamesQuery(
565        string $type,
566        bool $show_tot,
567        int $threshold,
568        int $number_of_surnames,
569        string $sorting
570    ): string {
571        $surnames = $this->topSurnames($number_of_surnames, $threshold);
572
573        switch ($sorting) {
574            default:
575            case 'alpha':
576                uksort($surnames, I18N::comparator());
577                break;
578            case 'count':
579                break;
580            case 'rcount':
581                $surnames = array_reverse($surnames, true);
582                break;
583        }
584
585        //find a module providing individual lists
586        $module = app(ModuleService::class)->findByComponent(ModuleListInterface::class, $this->tree, Auth::user())->first(static function (ModuleInterface $module): bool {
587            return $module instanceof IndividualListModule;
588        });
589
590        return FunctionsPrintLists::surnameList(
591            $surnames,
592            $type === 'list' ? 1 : 2,
593            $show_tot,
594            $module,
595            $this->tree
596        );
597    }
598
599    /**
600     * Find common surnames.
601     *
602     * @param int    $threshold
603     * @param int    $number_of_surnames
604     * @param string $sorting
605     *
606     * @return string
607     */
608    public function commonSurnames(
609        int $threshold = 1,
610        int $number_of_surnames = 10,
611        string $sorting = 'alpha'
612    ): string {
613        return $this->commonSurnamesQuery('nolist', false, $threshold, $number_of_surnames, $sorting);
614    }
615
616    /**
617     * Find common surnames.
618     *
619     * @param int    $threshold
620     * @param int    $number_of_surnames
621     * @param string $sorting
622     *
623     * @return string
624     */
625    public function commonSurnamesTotals(
626        int $threshold = 1,
627        int $number_of_surnames = 10,
628        string $sorting = 'count'
629    ): string {
630        return $this->commonSurnamesQuery('nolist', true, $threshold, $number_of_surnames, $sorting);
631    }
632
633    /**
634     * Find common surnames.
635     *
636     * @param int    $threshold
637     * @param int    $number_of_surnames
638     * @param string $sorting
639     *
640     * @return string
641     */
642    public function commonSurnamesList(
643        int $threshold = 1,
644        int $number_of_surnames = 10,
645        string $sorting = 'alpha'
646    ): string {
647        return $this->commonSurnamesQuery('list', false, $threshold, $number_of_surnames, $sorting);
648    }
649
650    /**
651     * Find common surnames.
652     *
653     * @param int    $threshold
654     * @param int    $number_of_surnames
655     * @param string $sorting
656     *
657     * @return string
658     */
659    public function commonSurnamesListTotals(
660        int $threshold = 1,
661        int $number_of_surnames = 10,
662        string $sorting = 'count'
663    ): string {
664        return $this->commonSurnamesQuery('list', true, $threshold, $number_of_surnames, $sorting);
665    }
666
667    /**
668     * Get a count of births by month.
669     *
670     * @param int  $year1
671     * @param int  $year2
672     *
673     * @return Builder
674     */
675    public function statsBirthQuery(int $year1 = -1, int $year2 = -1): Builder
676    {
677        $query = DB::table('dates')
678            ->select(['d_month', new Expression('COUNT(*) AS total')])
679            ->where('d_file', '=', $this->tree->id())
680            ->where('d_fact', '=', 'BIRT')
681            ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
682            ->groupBy(['d_month']);
683
684        if ($year1 >= 0 && $year2 >= 0) {
685            $query->whereBetween('d_year', [$year1, $year2]);
686        }
687
688        return $query;
689    }
690
691    /**
692     * Get a count of births by month.
693     *
694     * @param int  $year1
695     * @param int  $year2
696     *
697     * @return Builder
698     */
699    public function statsBirthBySexQuery(int $year1 = -1, int $year2 = -1): Builder
700    {
701        return $this->statsBirthQuery($year1, $year2)
702            ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')])
703            ->join('individuals', static function (JoinClause $join): void {
704                $join
705                    ->on('i_id', '=', 'd_gid')
706                    ->on('i_file', '=', 'd_file');
707            })
708            ->groupBy(['i_sex']);
709    }
710
711    /**
712     * General query on births.
713     *
714     * @param string|null $color_from
715     * @param string|null $color_to
716     *
717     * @return string
718     */
719    public function statsBirth(string $color_from = null, string $color_to = null): string
720    {
721        return (new ChartBirth($this->century_service, $this->color_service, $this->tree))
722            ->chartBirth($color_from, $color_to);
723    }
724
725    /**
726     * Get a list of death dates.
727     *
728     * @param int  $year1
729     * @param int  $year2
730     *
731     * @return Builder
732     */
733    public function statsDeathQuery(int $year1 = -1, int $year2 = -1): Builder
734    {
735        $query = DB::table('dates')
736            ->select(['d_month', new Expression('COUNT(*) AS total')])
737            ->where('d_file', '=', $this->tree->id())
738            ->where('d_fact', '=', 'DEAT')
739            ->whereIn('d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
740            ->groupBy(['d_month']);
741
742        if ($year1 >= 0 && $year2 >= 0) {
743            $query->whereBetween('d_year', [$year1, $year2]);
744        }
745
746        return $query;
747    }
748
749    /**
750     * Get a list of death dates.
751     *
752     * @param int  $year1
753     * @param int  $year2
754     *
755     * @return Builder
756     */
757    public function statsDeathBySexQuery(int $year1 = -1, int $year2 = -1): Builder
758    {
759        return $this->statsDeathQuery($year1, $year2)
760            ->select(['d_month', 'i_sex', new Expression('COUNT(*) AS total')])
761            ->join('individuals', static function (JoinClause $join): void {
762                $join
763                    ->on('i_id', '=', 'd_gid')
764                    ->on('i_file', '=', 'd_file');
765            })
766            ->groupBy(['i_sex']);
767    }
768
769    /**
770     * General query on deaths.
771     *
772     * @param string|null $color_from
773     * @param string|null $color_to
774     *
775     * @return string
776     */
777    public function statsDeath(string $color_from = null, string $color_to = null): string
778    {
779        return (new ChartDeath($this->century_service, $this->color_service, $this->tree))
780            ->chartDeath($color_from, $color_to);
781    }
782
783    /**
784     * General query on ages.
785     *
786     * @param string $related
787     * @param string $sex
788     * @param int    $year1
789     * @param int    $year2
790     *
791     * @return array<object>
792     */
793    public function statsAgeQuery(string $related = 'BIRT', string $sex = 'BOTH', int $year1 = -1, int $year2 = -1): array
794    {
795        $prefix = DB::connection()->getTablePrefix();
796
797        $query = $this->birthAndDeathQuery($sex);
798
799        if ($year1 >= 0 && $year2 >= 0) {
800            $query
801                ->whereIn('birth.d_type', ['@#DGREGORIAN@', '@#DJULIAN@'])
802                ->whereIn('death.d_type', ['@#DGREGORIAN@', '@#DJULIAN@']);
803
804            if ($related === 'BIRT') {
805                $query->whereBetween('birth.d_year', [$year1, $year2]);
806            } elseif ($related === 'DEAT') {
807                $query->whereBetween('death.d_year', [$year1, $year2]);
808            }
809        }
810
811        return $query
812            ->select(new Expression($prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS days'))
813            ->orderBy('days', 'desc')
814            ->get()
815            ->all();
816    }
817
818    /**
819     * General query on ages.
820     *
821     * @return string
822     */
823    public function statsAge(): string
824    {
825        return (new ChartAge($this->century_service, $this->tree))->chartAge();
826    }
827
828    /**
829     * Lifespan
830     *
831     * @param string $type
832     * @param string $sex
833     *
834     * @return string
835     */
836    private function longlifeQuery(string $type, string $sex): string
837    {
838        $prefix = DB::connection()->getTablePrefix();
839
840        $row = $this->birthAndDeathQuery($sex)
841            ->orderBy('days', 'desc')
842            ->select(['individuals.*', new Expression($prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1 AS days')])
843            ->first();
844
845        if ($row === null) {
846            return '';
847        }
848
849        /** @var Individual $individual */
850        $individual = Registry::individualFactory()->mapper($this->tree)($row);
851
852        if ($type !== 'age' && !$individual->canShow()) {
853            return I18N::translate('This information is private and cannot be shown.');
854        }
855
856        switch ($type) {
857            default:
858            case 'full':
859                return $individual->formatList();
860
861            case 'age':
862                return I18N::number((int) ($row->days / 365.25));
863
864            case 'name':
865                return '<a href="' . e($individual->url()) . '">' . $individual->fullName() . '</a>';
866        }
867    }
868
869    /**
870     * Find the longest lived individual.
871     *
872     * @return string
873     */
874    public function longestLife(): string
875    {
876        return $this->longlifeQuery('full', 'BOTH');
877    }
878
879    /**
880     * Find the age of the longest lived individual.
881     *
882     * @return string
883     */
884    public function longestLifeAge(): string
885    {
886        return $this->longlifeQuery('age', 'BOTH');
887    }
888
889    /**
890     * Find the name of the longest lived individual.
891     *
892     * @return string
893     */
894    public function longestLifeName(): string
895    {
896        return $this->longlifeQuery('name', 'BOTH');
897    }
898
899    /**
900     * Find the longest lived female.
901     *
902     * @return string
903     */
904    public function longestLifeFemale(): string
905    {
906        return $this->longlifeQuery('full', 'F');
907    }
908
909    /**
910     * Find the age of the longest lived female.
911     *
912     * @return string
913     */
914    public function longestLifeFemaleAge(): string
915    {
916        return $this->longlifeQuery('age', 'F');
917    }
918
919    /**
920     * Find the name of the longest lived female.
921     *
922     * @return string
923     */
924    public function longestLifeFemaleName(): string
925    {
926        return $this->longlifeQuery('name', 'F');
927    }
928
929    /**
930     * Find the longest lived male.
931     *
932     * @return string
933     */
934    public function longestLifeMale(): string
935    {
936        return $this->longlifeQuery('full', 'M');
937    }
938
939    /**
940     * Find the age of the longest lived male.
941     *
942     * @return string
943     */
944    public function longestLifeMaleAge(): string
945    {
946        return $this->longlifeQuery('age', 'M');
947    }
948
949    /**
950     * Find the name of the longest lived male.
951     *
952     * @return string
953     */
954    public function longestLifeMaleName(): string
955    {
956        return $this->longlifeQuery('name', 'M');
957    }
958
959    /**
960     * Returns the calculated age the time of event.
961     *
962     * @param int $days The age from the database record
963     *
964     * @return string
965     */
966    private function calculateAge(int $days): string
967    {
968        if ($days < 31) {
969            return I18N::plural('%s day', '%s days', $days, I18N::number($days));
970        }
971
972        if ($days < 365) {
973            $months = (int) ($days / 30.5);
974            return I18N::plural('%s month', '%s months', $months, I18N::number($months));
975        }
976
977        $years = (int) ($days / 365.25);
978
979        return I18N::plural('%s year', '%s years', $years, I18N::number($years));
980    }
981
982    /**
983     * Find the oldest individuals.
984     *
985     * @param string $sex
986     * @param int    $total
987     *
988     * @return array<array<string,mixed>>
989     */
990    private function topTenOldestQuery(string $sex, int $total): array
991    {
992        $prefix = DB::connection()->getTablePrefix();
993
994        $rows = $this->birthAndDeathQuery($sex)
995            ->groupBy(['i_id', 'i_file'])
996            ->orderBy('days', 'desc')
997            ->select(['individuals.*', new Expression('MAX(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) AS days')])
998            ->take($total)
999            ->get();
1000
1001        $top10 = [];
1002        foreach ($rows as $row) {
1003            /** @var Individual $individual */
1004            $individual = Registry::individualFactory()->mapper($this->tree)($row);
1005
1006            if ($individual->canShow()) {
1007                $top10[] = [
1008                    'person' => $individual,
1009                    'age'    => $this->calculateAge((int) $row->days),
1010                ];
1011            }
1012        }
1013
1014        return $top10;
1015    }
1016
1017    /**
1018     * Find the oldest individuals.
1019     *
1020     * @param int $total
1021     *
1022     * @return string
1023     */
1024    public function topTenOldest(int $total = 10): string
1025    {
1026        $records = $this->topTenOldestQuery('BOTH', $total);
1027
1028        return view('statistics/individuals/top10-nolist', [
1029            'records' => $records,
1030        ]);
1031    }
1032
1033    /**
1034     * Find the oldest living individuals.
1035     *
1036     * @param int $total
1037     *
1038     * @return string
1039     */
1040    public function topTenOldestList(int $total = 10): string
1041    {
1042        $records = $this->topTenOldestQuery('BOTH', $total);
1043
1044        return view('statistics/individuals/top10-list', [
1045            'records' => $records,
1046        ]);
1047    }
1048
1049    /**
1050     * Find the oldest females.
1051     *
1052     * @param int $total
1053     *
1054     * @return string
1055     */
1056    public function topTenOldestFemale(int $total = 10): string
1057    {
1058        $records = $this->topTenOldestQuery('F', $total);
1059
1060        return view('statistics/individuals/top10-nolist', [
1061            'records' => $records,
1062        ]);
1063    }
1064
1065    /**
1066     * Find the oldest living females.
1067     *
1068     * @param int $total
1069     *
1070     * @return string
1071     */
1072    public function topTenOldestFemaleList(int $total = 10): string
1073    {
1074        $records = $this->topTenOldestQuery('F', $total);
1075
1076        return view('statistics/individuals/top10-list', [
1077            'records' => $records,
1078        ]);
1079    }
1080
1081    /**
1082     * Find the longest lived males.
1083     *
1084     * @param int $total
1085     *
1086     * @return string
1087     */
1088    public function topTenOldestMale(int $total = 10): string
1089    {
1090        $records = $this->topTenOldestQuery('M', $total);
1091
1092        return view('statistics/individuals/top10-nolist', [
1093            'records' => $records,
1094        ]);
1095    }
1096
1097    /**
1098     * Find the longest lived males.
1099     *
1100     * @param int $total
1101     *
1102     * @return string
1103     */
1104    public function topTenOldestMaleList(int $total = 10): string
1105    {
1106        $records = $this->topTenOldestQuery('M', $total);
1107
1108        return view('statistics/individuals/top10-list', [
1109            'records' => $records,
1110        ]);
1111    }
1112
1113    /**
1114     * Find the oldest living individuals.
1115     *
1116     * @param string $sex   "M", "F" or "BOTH"
1117     * @param int    $total
1118     *
1119     * @return array<array<string,mixed>>
1120     */
1121    private function topTenOldestAliveQuery(string $sex, int $total): array
1122    {
1123        $query = DB::table('dates')
1124            ->join('individuals', static function (JoinClause $join): void {
1125                $join
1126                    ->on('i_id', '=', 'd_gid')
1127                    ->on('i_file', '=', 'd_file');
1128            })
1129            ->where('d_file', '=', $this->tree->id())
1130            ->where('d_julianday1', '<>', 0)
1131            ->where('d_fact', '=', 'BIRT')
1132            ->where('i_gedcom', 'NOT LIKE', "%\n1 DEAT%")
1133            ->where('i_gedcom', 'NOT LIKE', "%\n1 BURI%")
1134            ->where('i_gedcom', 'NOT LIKE', "%\n1 CREM%");
1135
1136        if ($sex === 'F' || $sex === 'M') {
1137            $query->where('i_sex', '=', $sex);
1138        }
1139
1140        return $query
1141            ->groupBy(['i_id', 'i_file'])
1142            ->orderBy(new Expression('MIN(d_julianday1)'))
1143            ->select(['individuals.*'])
1144            ->take($total)
1145            ->get()
1146            ->map(Registry::individualFactory()->mapper($this->tree))
1147            ->filter(GedcomRecord::accessFilter())
1148            ->map(function (Individual $individual): array {
1149                return [
1150                    'person' => $individual,
1151                    'age'    => $this->calculateAge(Carbon::now()->julianDay() - $individual->getBirthDate()->minimumJulianDay()),
1152                ];
1153            })
1154            ->all();
1155    }
1156
1157    /**
1158     * Find the oldest living individuals.
1159     *
1160     * @param int $total
1161     *
1162     * @return string
1163     */
1164    public function topTenOldestAlive(int $total = 10): string
1165    {
1166        if (!Auth::isMember($this->tree)) {
1167            return I18N::translate('This information is private and cannot be shown.');
1168        }
1169
1170        $records = $this->topTenOldestAliveQuery('BOTH', $total);
1171
1172        return view('statistics/individuals/top10-nolist', [
1173            'records' => $records,
1174        ]);
1175    }
1176
1177    /**
1178     * Find the oldest living individuals.
1179     *
1180     * @param int $total
1181     *
1182     * @return string
1183     */
1184    public function topTenOldestListAlive(int $total = 10): string
1185    {
1186        if (!Auth::isMember($this->tree)) {
1187            return I18N::translate('This information is private and cannot be shown.');
1188        }
1189
1190        $records = $this->topTenOldestAliveQuery('BOTH', $total);
1191
1192        return view('statistics/individuals/top10-list', [
1193            'records' => $records,
1194        ]);
1195    }
1196
1197    /**
1198     * Find the oldest living females.
1199     *
1200     * @param int $total
1201     *
1202     * @return string
1203     */
1204    public function topTenOldestFemaleAlive(int $total = 10): string
1205    {
1206        if (!Auth::isMember($this->tree)) {
1207            return I18N::translate('This information is private and cannot be shown.');
1208        }
1209
1210        $records = $this->topTenOldestAliveQuery('F', $total);
1211
1212        return view('statistics/individuals/top10-nolist', [
1213            'records' => $records,
1214        ]);
1215    }
1216
1217    /**
1218     * Find the oldest living females.
1219     *
1220     * @param int $total
1221     *
1222     * @return string
1223     */
1224    public function topTenOldestFemaleListAlive(int $total = 10): string
1225    {
1226        if (!Auth::isMember($this->tree)) {
1227            return I18N::translate('This information is private and cannot be shown.');
1228        }
1229
1230        $records = $this->topTenOldestAliveQuery('F', $total);
1231
1232        return view('statistics/individuals/top10-list', [
1233            'records' => $records,
1234        ]);
1235    }
1236
1237    /**
1238     * Find the longest lived living males.
1239     *
1240     * @param int $total
1241     *
1242     * @return string
1243     */
1244    public function topTenOldestMaleAlive(int $total = 10): string
1245    {
1246        if (!Auth::isMember($this->tree)) {
1247            return I18N::translate('This information is private and cannot be shown.');
1248        }
1249
1250        $records = $this->topTenOldestAliveQuery('M', $total);
1251
1252        return view('statistics/individuals/top10-nolist', [
1253            'records' => $records,
1254        ]);
1255    }
1256
1257    /**
1258     * Find the longest lived living males.
1259     *
1260     * @param int $total
1261     *
1262     * @return string
1263     */
1264    public function topTenOldestMaleListAlive(int $total = 10): string
1265    {
1266        if (!Auth::isMember($this->tree)) {
1267            return I18N::translate('This information is private and cannot be shown.');
1268        }
1269
1270        $records = $this->topTenOldestAliveQuery('M', $total);
1271
1272        return view('statistics/individuals/top10-list', [
1273            'records' => $records,
1274        ]);
1275    }
1276
1277    /**
1278     * Find the average lifespan.
1279     *
1280     * @param string $sex        "M", "F" or "BOTH"
1281     * @param bool   $show_years
1282     *
1283     * @return string
1284     */
1285    private function averageLifespanQuery(string $sex, bool $show_years): string
1286    {
1287        $prefix = DB::connection()->getTablePrefix();
1288
1289        $days = (int) $this->birthAndDeathQuery($sex)
1290            ->select(new Expression('AVG(' . $prefix . 'death.d_julianday2 - ' . $prefix . 'birth.d_julianday1) AS days'))
1291            ->value('days');
1292
1293        if ($show_years) {
1294            return $this->calculateAge($days);
1295        }
1296
1297        return I18N::number((int) ($days / 365.25));
1298    }
1299
1300    /**
1301     * Find the average lifespan.
1302     *
1303     * @param bool $show_years
1304     *
1305     * @return string
1306     */
1307    public function averageLifespan(bool $show_years): string
1308    {
1309        return $this->averageLifespanQuery('BOTH', $show_years);
1310    }
1311
1312    /**
1313     * Find the average lifespan of females.
1314     *
1315     * @param bool $show_years
1316     *
1317     * @return string
1318     */
1319    public function averageLifespanFemale(bool $show_years): string
1320    {
1321        return $this->averageLifespanQuery('F', $show_years);
1322    }
1323
1324    /**
1325     * Find the average male lifespan.
1326     *
1327     * @param bool $show_years
1328     *
1329     * @return string
1330     */
1331    public function averageLifespanMale(bool $show_years): string
1332    {
1333        return $this->averageLifespanQuery('M', $show_years);
1334    }
1335
1336    /**
1337     * Convert totals into percentages.
1338     *
1339     * @param int $count
1340     * @param int $total
1341     *
1342     * @return string
1343     */
1344    private function getPercentage(int $count, int $total): string
1345    {
1346        return $total !== 0 ? I18N::percentage($count / $total, 1) : '';
1347    }
1348
1349    /**
1350     * Returns how many individuals exist in the tree.
1351     *
1352     * @return int
1353     */
1354    private function totalIndividualsQuery(): int
1355    {
1356        return DB::table('individuals')
1357            ->where('i_file', '=', $this->tree->id())
1358            ->count();
1359    }
1360
1361    /**
1362     * Count the number of living individuals.
1363     *
1364     * The totalLiving/totalDeceased queries assume that every dead person will
1365     * have a DEAT record. It will not include individuals who were born more
1366     * than MAX_ALIVE_AGE years ago, and who have no DEAT record.
1367     * A good reason to run the “Add missing DEAT records” batch-update!
1368     *
1369     * @return int
1370     */
1371    private function totalLivingQuery(): int
1372    {
1373        $query = DB::table('individuals')
1374            ->where('i_file', '=', $this->tree->id());
1375
1376        foreach (Gedcom::DEATH_EVENTS as $death_event) {
1377            $query->where('i_gedcom', 'NOT LIKE', "%\n1 " . $death_event . '%');
1378        }
1379
1380        return $query->count();
1381    }
1382
1383    /**
1384     * Count the number of dead individuals.
1385     *
1386     * @return int
1387     */
1388    private function totalDeceasedQuery(): int
1389    {
1390        return DB::table('individuals')
1391            ->where('i_file', '=', $this->tree->id())
1392            ->where(static function (Builder $query): void {
1393                foreach (Gedcom::DEATH_EVENTS as $death_event) {
1394                    $query->orWhere('i_gedcom', 'LIKE', "%\n1 " . $death_event . '%');
1395                }
1396            })
1397            ->count();
1398    }
1399
1400    /**
1401     * Returns the total count of a specific sex.
1402     *
1403     * @param string $sex The sex to query
1404     *
1405     * @return int
1406     */
1407    private function getTotalSexQuery(string $sex): int
1408    {
1409        return DB::table('individuals')
1410            ->where('i_file', '=', $this->tree->id())
1411            ->where('i_sex', '=', $sex)
1412            ->count();
1413    }
1414
1415    /**
1416     * Returns the total number of males.
1417     *
1418     * @return int
1419     */
1420    private function totalSexMalesQuery(): int
1421    {
1422        return $this->getTotalSexQuery('M');
1423    }
1424
1425    /**
1426     * Returns the total number of females.
1427     *
1428     * @return int
1429     */
1430    private function totalSexFemalesQuery(): int
1431    {
1432        return $this->getTotalSexQuery('F');
1433    }
1434
1435    /**
1436     * Returns the total number of individuals with unknown sex.
1437     *
1438     * @return int
1439     */
1440    private function totalSexUnknownQuery(): int
1441    {
1442        return $this->getTotalSexQuery('U');
1443    }
1444
1445    /**
1446     * Count the total families.
1447     *
1448     * @return int
1449     */
1450    private function totalFamiliesQuery(): int
1451    {
1452        return DB::table('families')
1453            ->where('f_file', '=', $this->tree->id())
1454            ->count();
1455    }
1456
1457    /**
1458     * How many individuals have one or more sources.
1459     *
1460     * @return int
1461     */
1462    private function totalIndisWithSourcesQuery(): int
1463    {
1464        return DB::table('individuals')
1465            ->select(['i_id'])
1466            ->distinct()
1467            ->join('link', static function (JoinClause $join): void {
1468                $join->on('i_id', '=', 'l_from')
1469                    ->on('i_file', '=', 'l_file');
1470            })
1471            ->where('l_file', '=', $this->tree->id())
1472            ->where('l_type', '=', 'SOUR')
1473            ->count('i_id');
1474    }
1475
1476    /**
1477     * Count the families with source records.
1478     *
1479     * @return int
1480     */
1481    private function totalFamsWithSourcesQuery(): int
1482    {
1483        return DB::table('families')
1484            ->select(['f_id'])
1485            ->distinct()
1486            ->join('link', static function (JoinClause $join): void {
1487                $join->on('f_id', '=', 'l_from')
1488                    ->on('f_file', '=', 'l_file');
1489            })
1490            ->where('l_file', '=', $this->tree->id())
1491            ->where('l_type', '=', 'SOUR')
1492            ->count('f_id');
1493    }
1494
1495    /**
1496     * Count the number of repositories.
1497     *
1498     * @return int
1499     */
1500    private function totalRepositoriesQuery(): int
1501    {
1502        return DB::table('other')
1503            ->where('o_file', '=', $this->tree->id())
1504            ->where('o_type', '=', 'REPO')
1505            ->count();
1506    }
1507
1508    /**
1509     * Count the total number of sources.
1510     *
1511     * @return int
1512     */
1513    private function totalSourcesQuery(): int
1514    {
1515        return DB::table('sources')
1516            ->where('s_file', '=', $this->tree->id())
1517            ->count();
1518    }
1519
1520    /**
1521     * Count the number of notes.
1522     *
1523     * @return int
1524     */
1525    private function totalNotesQuery(): int
1526    {
1527        return DB::table('other')
1528            ->where('o_file', '=', $this->tree->id())
1529            ->where('o_type', '=', 'NOTE')
1530            ->count();
1531    }
1532
1533    /**
1534     * Returns the total number of records.
1535     *
1536     * @return int
1537     */
1538    private function totalRecordsQuery(): int
1539    {
1540        return $this->totalIndividualsQuery()
1541            + $this->totalFamiliesQuery()
1542            + $this->totalNotesQuery()
1543            + $this->totalRepositoriesQuery()
1544            + $this->totalSourcesQuery();
1545    }
1546
1547    /**
1548     * @return string
1549     */
1550    public function totalRecords(): string
1551    {
1552        return I18N::number($this->totalRecordsQuery());
1553    }
1554
1555    /**
1556     * @return string
1557     */
1558    public function totalIndividuals(): string
1559    {
1560        return I18N::number($this->totalIndividualsQuery());
1561    }
1562
1563    /**
1564     * Count the number of living individuals.
1565     *
1566     * @return string
1567     */
1568    public function totalLiving(): string
1569    {
1570        return I18N::number($this->totalLivingQuery());
1571    }
1572
1573    /**
1574     * Count the number of dead individuals.
1575     *
1576     * @return string
1577     */
1578    public function totalDeceased(): string
1579    {
1580        return I18N::number($this->totalDeceasedQuery());
1581    }
1582
1583    /**
1584     * @return string
1585     */
1586    public function totalSexMales(): string
1587    {
1588        return I18N::number($this->totalSexMalesQuery());
1589    }
1590
1591    /**
1592     * @return string
1593     */
1594    public function totalSexFemales(): string
1595    {
1596        return I18N::number($this->totalSexFemalesQuery());
1597    }
1598
1599    /**
1600     * @return string
1601     */
1602    public function totalSexUnknown(): string
1603    {
1604        return I18N::number($this->totalSexUnknownQuery());
1605    }
1606
1607    /**
1608     * @return string
1609     */
1610    public function totalFamilies(): string
1611    {
1612        return I18N::number($this->totalFamiliesQuery());
1613    }
1614
1615    /**
1616     * How many individuals have one or more sources.
1617     *
1618     * @return string
1619     */
1620    public function totalIndisWithSources(): string
1621    {
1622        return I18N::number($this->totalIndisWithSourcesQuery());
1623    }
1624
1625    /**
1626     * Count the families with with source records.
1627     *
1628     * @return string
1629     */
1630    public function totalFamsWithSources(): string
1631    {
1632        return I18N::number($this->totalFamsWithSourcesQuery());
1633    }
1634
1635    /**
1636     * @return string
1637     */
1638    public function totalRepositories(): string
1639    {
1640        return I18N::number($this->totalRepositoriesQuery());
1641    }
1642
1643    /**
1644     * @return string
1645     */
1646    public function totalSources(): string
1647    {
1648        return I18N::number($this->totalSourcesQuery());
1649    }
1650
1651    /**
1652     * @return string
1653     */
1654    public function totalNotes(): string
1655    {
1656        return I18N::number($this->totalNotesQuery());
1657    }
1658
1659    /**
1660     * @return string
1661     */
1662    public function totalIndividualsPercentage(): string
1663    {
1664        return $this->getPercentage(
1665            $this->totalIndividualsQuery(),
1666            $this->totalRecordsQuery()
1667        );
1668    }
1669
1670    /**
1671     * @return string
1672     */
1673    public function totalFamiliesPercentage(): string
1674    {
1675        return $this->getPercentage(
1676            $this->totalFamiliesQuery(),
1677            $this->totalRecordsQuery()
1678        );
1679    }
1680
1681    /**
1682     * @return string
1683     */
1684    public function totalRepositoriesPercentage(): string
1685    {
1686        return $this->getPercentage(
1687            $this->totalRepositoriesQuery(),
1688            $this->totalRecordsQuery()
1689        );
1690    }
1691
1692    /**
1693     * @return string
1694     */
1695    public function totalSourcesPercentage(): string
1696    {
1697        return $this->getPercentage(
1698            $this->totalSourcesQuery(),
1699            $this->totalRecordsQuery()
1700        );
1701    }
1702
1703    /**
1704     * @return string
1705     */
1706    public function totalNotesPercentage(): string
1707    {
1708        return $this->getPercentage(
1709            $this->totalNotesQuery(),
1710            $this->totalRecordsQuery()
1711        );
1712    }
1713
1714    /**
1715     * @return string
1716     */
1717    public function totalLivingPercentage(): string
1718    {
1719        return $this->getPercentage(
1720            $this->totalLivingQuery(),
1721            $this->totalIndividualsQuery()
1722        );
1723    }
1724
1725    /**
1726     * @return string
1727     */
1728    public function totalDeceasedPercentage(): string
1729    {
1730        return $this->getPercentage(
1731            $this->totalDeceasedQuery(),
1732            $this->totalIndividualsQuery()
1733        );
1734    }
1735
1736    /**
1737     * @return string
1738     */
1739    public function totalSexMalesPercentage(): string
1740    {
1741        return $this->getPercentage(
1742            $this->totalSexMalesQuery(),
1743            $this->totalIndividualsQuery()
1744        );
1745    }
1746
1747    /**
1748     * @return string
1749     */
1750    public function totalSexFemalesPercentage(): string
1751    {
1752        return $this->getPercentage(
1753            $this->totalSexFemalesQuery(),
1754            $this->totalIndividualsQuery()
1755        );
1756    }
1757
1758    /**
1759     * @return string
1760     */
1761    public function totalSexUnknownPercentage(): string
1762    {
1763        return $this->getPercentage(
1764            $this->totalSexUnknownQuery(),
1765            $this->totalIndividualsQuery()
1766        );
1767    }
1768
1769    /**
1770     * Create a chart of common given names.
1771     *
1772     * @param string|null $color_from
1773     * @param string|null $color_to
1774     * @param int         $maxtoshow
1775     *
1776     * @return string
1777     */
1778    public function chartCommonGiven(
1779        string $color_from = null,
1780        string $color_to = null,
1781        int $maxtoshow = 7
1782    ): string {
1783        $tot_indi = $this->totalIndividualsQuery();
1784        $given    = $this->commonGivenQuery('B', 'chart', false, 1, $maxtoshow);
1785
1786        if ($given === []) {
1787            return I18N::translate('This information is not available.');
1788        }
1789
1790        return (new ChartCommonGiven($this->color_service))
1791            ->chartCommonGiven($tot_indi, $given, $color_from, $color_to);
1792    }
1793
1794    /**
1795     * Create a chart of common surnames.
1796     *
1797     * @param string|null $color_from
1798     * @param string|null $color_to
1799     * @param int         $number_of_surnames
1800     *
1801     * @return string
1802     */
1803    public function chartCommonSurnames(
1804        string $color_from = null,
1805        string $color_to = null,
1806        int $number_of_surnames = 10
1807    ): string {
1808        $tot_indi     = $this->totalIndividualsQuery();
1809        $all_surnames = $this->topSurnames($number_of_surnames, 0);
1810
1811        if ($all_surnames === []) {
1812            return I18N::translate('This information is not available.');
1813        }
1814
1815        $surname_tradition = SurnameTradition::create($this->tree->getPreference('SURNAME_TRADITION'));
1816
1817        return (new ChartCommonSurname($this->color_service, $surname_tradition))
1818            ->chartCommonSurnames($tot_indi, $all_surnames, $color_from, $color_to);
1819    }
1820
1821    /**
1822     * Create a chart showing mortality.
1823     *
1824     * @param string|null $color_living
1825     * @param string|null $color_dead
1826     *
1827     * @return string
1828     */
1829    public function chartMortality(string $color_living = null, string $color_dead = null): string
1830    {
1831        $tot_l = $this->totalLivingQuery();
1832        $tot_d = $this->totalDeceasedQuery();
1833
1834        return (new ChartMortality($this->color_service))
1835            ->chartMortality($tot_l, $tot_d, $color_living, $color_dead);
1836    }
1837
1838    /**
1839     * Create a chart showing individuals with/without sources.
1840     *
1841     * @param string|null $color_from
1842     * @param string|null $color_to
1843     *
1844     * @return string
1845     */
1846    public function chartIndisWithSources(
1847        string $color_from = null,
1848        string $color_to = null
1849    ): string {
1850        $tot_indi        = $this->totalIndividualsQuery();
1851        $tot_indi_source = $this->totalIndisWithSourcesQuery();
1852
1853        return (new ChartIndividualWithSources($this->color_service))
1854            ->chartIndisWithSources($tot_indi, $tot_indi_source, $color_from, $color_to);
1855    }
1856
1857    /**
1858     * Create a chart of individuals with/without sources.
1859     *
1860     * @param string|null $color_from
1861     * @param string|null $color_to
1862     *
1863     * @return string
1864     */
1865    public function chartFamsWithSources(
1866        string $color_from = null,
1867        string $color_to = null
1868    ): string {
1869        $tot_fam        = $this->totalFamiliesQuery();
1870        $tot_fam_source = $this->totalFamsWithSourcesQuery();
1871
1872        return (new ChartFamilyWithSources($this->color_service))
1873            ->chartFamsWithSources($tot_fam, $tot_fam_source, $color_from, $color_to);
1874    }
1875
1876    /**
1877     * @param string|null $color_female
1878     * @param string|null $color_male
1879     * @param string|null $color_unknown
1880     *
1881     * @return string
1882     */
1883    public function chartSex(
1884        string $color_female = null,
1885        string $color_male = null,
1886        string $color_unknown = null
1887    ): string {
1888        $tot_m = $this->totalSexMalesQuery();
1889        $tot_f = $this->totalSexFemalesQuery();
1890        $tot_u = $this->totalSexUnknownQuery();
1891
1892        return (new ChartSex())
1893            ->chartSex($tot_m, $tot_f, $tot_u, $color_female, $color_male, $color_unknown);
1894    }
1895
1896    /**
1897     * Query individuals, with their births and deaths.
1898     *
1899     * @param string $sex
1900     *
1901     * @return Builder
1902     */
1903    private function birthAndDeathQuery(string $sex): Builder
1904    {
1905        $query = DB::table('individuals')
1906            ->where('i_file', '=', $this->tree->id())
1907            ->join('dates AS birth', static function (JoinClause $join): void {
1908                $join
1909                    ->on('birth.d_file', '=', 'i_file')
1910                    ->on('birth.d_gid', '=', 'i_id');
1911            })
1912            ->join('dates AS death', static function (JoinClause $join): void {
1913                $join
1914                    ->on('death.d_file', '=', 'i_file')
1915                    ->on('death.d_gid', '=', 'i_id');
1916            })
1917            ->where('birth.d_fact', '=', 'BIRT')
1918            ->where('death.d_fact', '=', 'DEAT')
1919            ->whereColumn('death.d_julianday1', '>=', 'birth.d_julianday2')
1920            ->where('birth.d_julianday2', '<>', 0);
1921
1922        if ($sex === 'M' || $sex === 'F') {
1923            $query->where('i_sex', '=', $sex);
1924        }
1925
1926        return $query;
1927    }
1928}
1929