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