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