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