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