StatisticsChartModule::postCustomChartAction()   F
last analyzed

Complexity

Conditions 99
Paths 120

Size

Total Lines 505
Code Lines 389

Duplication

Lines 0
Ratio 0 %

Importance

Changes 0
Metric Value
cc 99
eloc 389
c 0
b 0
f 0
nc 120
nop 1
dl 0
loc 505
rs 3.1999

How to fix   Long Method    Complexity   

Long Method

Small methods make your code easier to understand, in particular if combined with a good name. Besides, if your method is small, finding a good name is usually much easier.

For example, if you find yourself adding comments to a method's body, this is usually a good sign to extract the commented part to a new method, and use the comment as a starting point when coming up with a good name for this new method.

Commonly applied refactorings include:

1
<?php
2
3
/**
4
 * webtrees: online genealogy
5
 * Copyright (C) 2025 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
18
declare(strict_types=1);
19
20
namespace Fisharebest\Webtrees\Module;
21
22
use Fisharebest\Webtrees\Auth;
23
use Fisharebest\Webtrees\Http\Exceptions\HttpNotFoundException;
24
use Fisharebest\Webtrees\I18N;
25
use Fisharebest\Webtrees\Individual;
26
use Fisharebest\Webtrees\Services\UserService;
27
use Fisharebest\Webtrees\Statistics;
28
use Fisharebest\Webtrees\StatisticsData;
29
use Fisharebest\Webtrees\Validator;
30
use Psr\Http\Message\ResponseInterface;
31
use Psr\Http\Message\ServerRequestInterface;
32
33
use function app;
34
use function array_key_exists;
35
use function array_key_last;
36
use function array_keys;
37
use function array_map;
38
use function array_merge;
39
use function array_sum;
40
use function array_values;
41
use function array_walk;
42
use function assert;
43
use function count;
44
use function explode;
45
use function in_array;
46
use function is_numeric;
47
use function sprintf;
48
49
class StatisticsChartModule extends AbstractModule implements ModuleChartInterface
50
{
51
    use ModuleChartTrait;
52
53
    public const X_AXIS_INDIVIDUAL_MAP        = 1;
54
    public const X_AXIS_BIRTH_MAP             = 2;
55
    public const X_AXIS_DEATH_MAP             = 3;
56
    public const X_AXIS_MARRIAGE_MAP          = 4;
57
    public const X_AXIS_BIRTH_MONTH           = 11;
58
    public const X_AXIS_DEATH_MONTH           = 12;
59
    public const X_AXIS_MARRIAGE_MONTH        = 13;
60
    public const X_AXIS_FIRST_CHILD_MONTH     = 14;
61
    public const X_AXIS_FIRST_MARRIAGE_MONTH  = 15;
62
    public const X_AXIS_AGE_AT_DEATH          = 18;
63
    public const X_AXIS_AGE_AT_MARRIAGE       = 19;
64
    public const X_AXIS_AGE_AT_FIRST_MARRIAGE = 20;
65
    public const X_AXIS_NUMBER_OF_CHILDREN    = 21;
66
67
    public const Y_AXIS_NUMBERS = 201;
68
    public const Y_AXIS_PERCENT = 202;
69
70
    public const Z_AXIS_ALL  = 300;
71
    public const Z_AXIS_SEX  = 301;
72
    public const Z_AXIS_TIME = 302;
73
74
    // First two colors are blue/pink, to work with Z_AXIS_SEX.
75
    private const Z_AXIS_COLORS = ['0000FF', 'FFA0CB', '9F00FF', 'FF7000', '905030', 'FF0000', '00FF00', 'F0F000'];
76
77
    private const DAYS_IN_YEAR = 365.25;
78
79
    public function title(): string
80
    {
81
        /* I18N: Name of a module/chart */
82
        return I18N::translate('Statistics');
83
    }
84
85
    public function description(): string
86
    {
87
        /* I18N: Description of the “StatisticsChart” module */
88
        return I18N::translate('Various statistics charts.');
89
    }
90
91
    /**
92
     * CSS class for the URL.
93
     *
94
     * @return string
95
     */
96
    public function chartMenuClass(): string
97
    {
98
        return 'menu-chart-statistics';
99
    }
100
101
    /**
102
     * The URL for this chart.
103
     *
104
     * @param Individual                                $individual
105
     * @param array<bool|int|string|array<string>|null> $parameters
106
     *
107
     * @return string
108
     */
109
    public function chartUrl(Individual $individual, array $parameters = []): string
110
    {
111
        return route('module', [
112
                'module' => $this->name(),
113
                'action' => 'Chart',
114
                'tree'    => $individual->tree()->name(),
115
            ] + $parameters);
116
    }
117
118
    /**
119
     * A form to request the chart parameters.
120
     *
121
     * @param ServerRequestInterface $request
122
     *
123
     * @return ResponseInterface
124
     */
125
    public function getChartAction(ServerRequestInterface $request): ResponseInterface
126
    {
127
        $tree = Validator::attributes($request)->tree();
128
        $user = Validator::attributes($request)->user();
129
130
        Auth::checkComponentAccess($this, ModuleChartInterface::class, $tree, $user);
131
132
        $tabs = [
133
            I18N::translate('Individuals') => route('module', [
134
                'module' => $this->name(),
135
                'action' => 'Individuals',
136
                'tree'    => $tree->name(),
137
            ]),
138
            I18N::translate('Families')    => route('module', [
139
                'module' => $this->name(),
140
                'action' => 'Families',
141
                'tree'    => $tree->name(),
142
            ]),
143
            I18N::translate('Other')       => route('module', [
144
                'module' => $this->name(),
145
                'action' => 'Other',
146
                'tree'    => $tree->name(),
147
            ]),
148
            I18N::translate('Custom')      => route('module', [
149
                'module' => $this->name(),
150
                'action' => 'Custom',
151
                'tree'    => $tree->name(),
152
            ]),
153
        ];
154
155
        return $this->viewResponse('modules/statistics-chart/page', [
156
            'module' => $this->name(),
157
            'tabs'   => $tabs,
158
            'title'  => $this->title(),
159
            'tree'   => $tree,
160
        ]);
161
    }
162
163
    /**
164
     * @param ServerRequestInterface $request
165
     *
166
     * @return ResponseInterface
167
     */
168
    public function getIndividualsAction(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

168
    public function getIndividualsAction(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
169
    {
170
        $this->layout = 'layouts/ajax';
171
172
        return $this->viewResponse('modules/statistics-chart/individuals', [
173
            'show_oldest_living' => Auth::check(),
174
            'statistics'         => app(Statistics::class),
175
        ]);
176
    }
177
178
    /**
179
     * @param ServerRequestInterface $request
180
     *
181
     * @return ResponseInterface
182
     */
183
    public function getFamiliesAction(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

183
    public function getFamiliesAction(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
184
    {
185
        $this->layout = 'layouts/ajax';
186
187
        return $this->viewResponse('modules/statistics-chart/families', [
188
            'statistics' => app(Statistics::class),
189
        ]);
190
    }
191
192
    /**
193
     * @param ServerRequestInterface $request
194
     *
195
     * @return ResponseInterface
196
     */
197
    public function getOtherAction(ServerRequestInterface $request): ResponseInterface
0 ignored issues
show
Unused Code introduced by
The parameter $request is not used and could be removed. ( Ignorable by Annotation )

If this is a false-positive, you can also ignore this issue in your code via the ignore-unused  annotation

197
    public function getOtherAction(/** @scrutinizer ignore-unused */ ServerRequestInterface $request): ResponseInterface

This check looks for parameters that have been defined for a function or method, but which are not used in the method body.

Loading history...
198
    {
199
        $this->layout = 'layouts/ajax';
200
201
        return $this->viewResponse('modules/statistics-chart/other', [
202
            'statistics' => app(Statistics::class),
203
        ]);
204
    }
205
206
    /**
207
     * @param ServerRequestInterface $request
208
     *
209
     * @return ResponseInterface
210
     */
211
    public function getCustomAction(ServerRequestInterface $request): ResponseInterface
212
    {
213
        $this->layout = 'layouts/ajax';
214
215
        $tree = Validator::attributes($request)->tree();
216
217
        return $this->viewResponse('modules/statistics-chart/custom', [
218
            'module' => $this,
219
            'tree'   => $tree,
220
        ]);
221
    }
222
223
    /**
224
     * @param ServerRequestInterface $request
225
     *
226
     * @return ResponseInterface
227
     */
228
    public function postCustomChartAction(ServerRequestInterface $request): ResponseInterface
229
    {
230
        $tree = Validator::attributes($request)->tree();
231
232
        $statistics = app(Statistics::class);
233
        assert($statistics instanceof Statistics);
234
235
        $statistics_data = new StatisticsData($tree, new UserService());
236
237
        $x_axis_type = Validator::parsedBody($request)->integer('x-as');
238
        $y_axis_type = Validator::parsedBody($request)->integer('y-as');
239
        $z_axis_type = Validator::parsedBody($request)->integer('z-as');
240
        $ydata       = [];
241
242
        switch ($x_axis_type) {
243
            case self::X_AXIS_INDIVIDUAL_MAP:
244
                return response($statistics->chartDistribution(
245
                    Validator::parsedBody($request)->string('chart_shows'),
246
                    Validator::parsedBody($request)->string('chart_type'),
247
                    Validator::parsedBody($request)->string('SURN')
248
                ));
249
250
            case self::X_AXIS_BIRTH_MAP:
251
                return response($statistics->chartDistribution(
252
                    Validator::parsedBody($request)->string('chart_shows'),
253
                    'birth_distribution_chart'
254
                ));
255
256
            case self::X_AXIS_DEATH_MAP:
257
                return response($statistics->chartDistribution(
258
                    Validator::parsedBody($request)->string('chart_shows'),
259
                    'death_distribution_chart'
260
                ));
261
262
            case self::X_AXIS_MARRIAGE_MAP:
263
                return response($statistics->chartDistribution(
264
                    Validator::parsedBody($request)->string('chart_shows'),
265
                    'marriage_distribution_chart'
266
                ));
267
268
            case self::X_AXIS_BIRTH_MONTH:
269
                $chart_title  = I18N::translate('Month of birth');
270
                $x_axis_title = I18N::translate('Month');
271
                $x_axis       = $this->axisMonths();
272
273
                switch ($y_axis_type) {
274
                    case self::Y_AXIS_NUMBERS:
275
                        $y_axis_title = I18N::translate('Individuals');
276
                        break;
277
                    case self::Y_AXIS_PERCENT:
278
                        $y_axis_title = '%';
279
                        break;
280
                    default:
281
                        throw new HttpNotFoundException();
282
                }
283
284
                switch ($z_axis_type) {
285
                    case self::Z_AXIS_ALL:
286
                        $z_axis = $this->axisAll();
287
                        $rows   = $statistics_data->countEventsByMonth('BIRT', 0, 0);
288
                        foreach ($rows as $month => $total) {
289
                            $this->fillYData($month, 0, $total, $x_axis, $z_axis, $ydata);
290
                        }
291
                        break;
292
                    case self::Z_AXIS_SEX:
293
                        $z_axis = $this->axisSexes();
294
                        $rows   = $statistics_data->countEventsByMonthAndSex('BIRT', 0, 0);
295
                        foreach ($rows as $row) {
296
                            $this->fillYData($row->month, $row->sex, $row->total, $x_axis, $z_axis, $ydata);
297
                        }
298
                        break;
299
                    case self::Z_AXIS_TIME:
300
                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
301
                        $z_axis         = $this->axisYears($boundaries_csv);
302
                        $prev_boundary  = 0;
303
                        foreach (array_keys($z_axis) as $boundary) {
304
                            $rows = $statistics_data->countEventsByMonth('BIRT', $prev_boundary, $boundary);
305
                            foreach ($rows as $month => $total) {
306
                                $this->fillYData($month, $boundary, $total, $x_axis, $z_axis, $ydata);
307
                            }
308
                            $prev_boundary = $boundary + 1;
309
                        }
310
                        break;
311
                    default:
312
                        throw new HttpNotFoundException();
313
                }
314
315
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
316
317
            case self::X_AXIS_DEATH_MONTH:
318
                $chart_title  = I18N::translate('Month of death');
319
                $x_axis_title = I18N::translate('Month');
320
                $x_axis       = $this->axisMonths();
321
322
                switch ($y_axis_type) {
323
                    case self::Y_AXIS_NUMBERS:
324
                        $y_axis_title = I18N::translate('Individuals');
325
                        break;
326
                    case self::Y_AXIS_PERCENT:
327
                        $y_axis_title = '%';
328
                        break;
329
                    default:
330
                        throw new HttpNotFoundException();
331
                }
332
333
                switch ($z_axis_type) {
334
                    case self::Z_AXIS_ALL:
335
                        $z_axis = $this->axisAll();
336
                        $rows   = $statistics_data->countEventsByMonth('DEAT', 0, 0);
337
                        foreach ($rows as $month => $total) {
338
                            $this->fillYData($month, 0, $total, $x_axis, $z_axis, $ydata);
339
                        }
340
                        break;
341
                    case self::Z_AXIS_SEX:
342
                        $z_axis = $this->axisSexes();
343
                        $rows   = $statistics_data->countEventsByMonthAndSex('DEAT', 0, 0);
344
                        foreach ($rows as $row) {
345
                            $this->fillYData($row->month, $row->sex, $row->total, $x_axis, $z_axis, $ydata);
346
                        }
347
                        break;
348
                    case self::Z_AXIS_TIME:
349
                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
350
                        $z_axis         = $this->axisYears($boundaries_csv);
351
                        $prev_boundary  = 0;
352
                        foreach (array_keys($z_axis) as $boundary) {
353
                            $rows = $statistics_data->countEventsByMonth('DEAT', $prev_boundary, $boundary);
354
                            foreach ($rows as $month => $total) {
355
                                $this->fillYData($month, $boundary, $total, $x_axis, $z_axis, $ydata);
356
                            }
357
                            $prev_boundary = $boundary + 1;
358
                        }
359
                        break;
360
                    default:
361
                        throw new HttpNotFoundException();
362
                }
363
364
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
365
366
            case self::X_AXIS_MARRIAGE_MONTH:
367
                $chart_title  = I18N::translate('Month of marriage');
368
                $x_axis_title = I18N::translate('Month');
369
                $x_axis       = $this->axisMonths();
370
371
                switch ($y_axis_type) {
372
                    case self::Y_AXIS_NUMBERS:
373
                        $y_axis_title = I18N::translate('Families');
374
                        break;
375
                    case self::Y_AXIS_PERCENT:
376
                        $y_axis_title = '%';
377
                        break;
378
                    default:
379
                        throw new HttpNotFoundException();
380
                }
381
382
                switch ($z_axis_type) {
383
                    case self::Z_AXIS_ALL:
384
                        $z_axis = $this->axisAll();
385
                        $rows   = $statistics_data->countEventsByMonth('MARR', 0, 0);
386
                        foreach ($rows as $month => $total) {
387
                            $this->fillYData($month, 0, $total, $x_axis, $z_axis, $ydata);
388
                        }
389
                        break;
390
                    case self::Z_AXIS_TIME:
391
                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
392
                        $z_axis         = $this->axisYears($boundaries_csv);
393
                        $prev_boundary  = 0;
394
                        foreach (array_keys($z_axis) as $boundary) {
395
                            $rows = $statistics_data->countEventsByMonth('MARR', $prev_boundary, $boundary);
396
                            foreach ($rows as $month => $total) {
397
                                $this->fillYData($month, $boundary, $total, $x_axis, $z_axis, $ydata);
398
                            }
399
                            $prev_boundary = $boundary + 1;
400
                        }
401
                        break;
402
                    default:
403
                        throw new HttpNotFoundException();
404
                }
405
406
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
407
408
            case self::X_AXIS_FIRST_CHILD_MONTH:
409
                $chart_title  = I18N::translate('Month of birth of first child in a relation');
410
                $x_axis_title = I18N::translate('Month');
411
                $x_axis       = $this->axisMonths();
412
413
                switch ($y_axis_type) {
414
                    case self::Y_AXIS_NUMBERS:
415
                        $y_axis_title = I18N::translate('Children');
416
                        break;
417
                    case self::Y_AXIS_PERCENT:
418
                        $y_axis_title = '%';
419
                        break;
420
                    default:
421
                        throw new HttpNotFoundException();
422
                }
423
424
                switch ($z_axis_type) {
425
                    case self::Z_AXIS_ALL:
426
                        $z_axis = $this->axisAll();
427
                        $rows   = $statistics_data->countFirstChildrenByMonth(0, 0);
428
                        foreach ($rows as $month => $total) {
429
                            $this->fillYData($month, 0, $total, $x_axis, $z_axis, $ydata);
430
                        }
431
                        break;
432
                    case self::Z_AXIS_SEX:
433
                        $z_axis = $this->axisSexes();
434
                        $rows   = $statistics_data->countFirstChildrenByMonthAndSex(0, 0);
435
                        foreach ($rows as $row) {
436
                            $this->fillYData($row->month, $row->sex, $row->total, $x_axis, $z_axis, $ydata);
437
                        }
438
                        break;
439
                    case self::Z_AXIS_TIME:
440
                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
441
                        $z_axis         = $this->axisYears($boundaries_csv);
442
                        $prev_boundary  = 0;
443
                        foreach (array_keys($z_axis) as $boundary) {
444
                            $rows = $statistics_data->countFirstChildrenByMonth($prev_boundary, $boundary);
445
                            foreach ($rows as $month => $total) {
446
                                $this->fillYData($month, $boundary, $total, $x_axis, $z_axis, $ydata);
447
                            }
448
                            $prev_boundary = $boundary + 1;
449
                        }
450
                        break;
451
                    default:
452
                        throw new HttpNotFoundException();
453
                }
454
455
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
456
457
            case self::X_AXIS_FIRST_MARRIAGE_MONTH:
458
                $chart_title  = I18N::translate('Month of first marriage');
459
                $x_axis_title = I18N::translate('Month');
460
                $x_axis       = $this->axisMonths();
461
462
                switch ($y_axis_type) {
463
                    case self::Y_AXIS_NUMBERS:
464
                        $y_axis_title = I18N::translate('Families');
465
                        break;
466
                    case self::Y_AXIS_PERCENT:
467
                        $y_axis_title = '%';
468
                        break;
469
                    default:
470
                        throw new HttpNotFoundException();
471
                }
472
473
                switch ($z_axis_type) {
474
                    case self::Z_AXIS_ALL:
475
                        $z_axis = $this->axisAll();
476
                        $rows   = $statistics_data->countFirstMarriagesByMonth($tree, 0, 0);
477
                        foreach ($rows as $month => $total) {
478
                            $this->fillYData($month, 0, $total, $x_axis, $z_axis, $ydata);
479
                        }
480
                        break;
481
                    case self::Z_AXIS_TIME:
482
                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
483
                        $z_axis         = $this->axisYears($boundaries_csv);
484
                        $prev_boundary  = 0;
485
                        foreach (array_keys($z_axis) as $boundary) {
486
                            $rows = $statistics_data->countFirstMarriagesByMonth($tree, $prev_boundary, $boundary);
487
                            foreach ($rows as $month => $total) {
488
                                $this->fillYData($month, 0, $total, $x_axis, $z_axis, $ydata);
489
                            }
490
                            $prev_boundary = $boundary + 1;
491
                        }
492
                        break;
493
                    default:
494
                        throw new HttpNotFoundException();
495
                }
496
497
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
498
499
            case self::X_AXIS_AGE_AT_DEATH:
500
                $chart_title    = I18N::translate('Average age at death');
501
                $x_axis_title   = I18N::translate('age');
502
                $boundaries_csv = Validator::parsedBody($request)->string('x-axis-boundaries-ages');
503
                $x_axis         = $this->axisNumbers($boundaries_csv);
504
505
                switch ($y_axis_type) {
506
                    case self::Y_AXIS_NUMBERS:
507
                        $y_axis_title = I18N::translate('Individuals');
508
                        break;
509
                    case self::Y_AXIS_PERCENT:
510
                        $y_axis_title = '%';
511
                        break;
512
                    default:
513
                        throw new HttpNotFoundException();
514
                }
515
516
                switch ($z_axis_type) {
517
                    case self::Z_AXIS_ALL:
518
                        $z_axis = $this->axisAll();
519
                        $rows   = $statistics_data->statsAgeQuery('ALL', 0, 0);
520
                        foreach ($rows as $row) {
521
                            $years = (int) ($row->days / self::DAYS_IN_YEAR);
522
                            $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
523
                        }
524
                        break;
525
                    case self::Z_AXIS_SEX:
526
                        $z_axis = $this->axisSexes();
527
                        foreach (array_keys($z_axis) as $sex) {
528
                            $rows = $statistics_data->statsAgeQuery($sex, 0, 0);
529
                            foreach ($rows as $row) {
530
                                $years = (int) ($row->days / self::DAYS_IN_YEAR);
531
                                $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
532
                            }
533
                        }
534
                        break;
535
                    case self::Z_AXIS_TIME:
536
                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
537
                        $z_axis         = $this->axisYears($boundaries_csv);
538
                        $prev_boundary  = 0;
539
                        foreach (array_keys($z_axis) as $boundary) {
540
                            $rows = $statistics_data->statsAgeQuery('ALL', $prev_boundary, $boundary);
541
                            foreach ($rows as $row) {
542
                                $years = (int) ($row->days / self::DAYS_IN_YEAR);
543
                                $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
544
                            }
545
                            $prev_boundary = $boundary + 1;
546
                        }
547
548
                        break;
549
                    default:
550
                        throw new HttpNotFoundException();
551
                }
552
553
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
554
555
            case self::X_AXIS_AGE_AT_MARRIAGE:
556
                $chart_title    = I18N::translate('Age in year of marriage');
557
                $x_axis_title   = I18N::translate('age');
558
                $boundaries_csv = Validator::parsedBody($request)->string('x-axis-boundaries-ages_m');
559
                $x_axis         = $this->axisNumbers($boundaries_csv);
560
561
                switch ($y_axis_type) {
562
                    case self::Y_AXIS_NUMBERS:
563
                        $y_axis_title = I18N::translate('Individuals');
564
                        break;
565
                    case self::Y_AXIS_PERCENT:
566
                        $y_axis_title = '%';
567
                        break;
568
                    default:
569
                        throw new HttpNotFoundException();
570
                }
571
572
                switch ($z_axis_type) {
573
                    case self::Z_AXIS_ALL:
574
                        $z_axis = $this->axisAll();
575
                        // The stats query doesn't have an "all" function, so query M/F separately
576
                        foreach (['M', 'F'] as $sex) {
577
                            $rows = $statistics->statsMarrAgeQuery($sex);
578
                            foreach ($rows as $row) {
579
                                $years = (int) ($row->age / self::DAYS_IN_YEAR);
580
                                $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
581
                            }
582
                        }
583
                        break;
584
                    case self::Z_AXIS_SEX:
585
                        $z_axis = $this->axisSexes();
586
                        foreach (array_keys($z_axis) as $sex) {
587
                            $rows = $statistics->statsMarrAgeQuery($sex);
588
                            foreach ($rows as $row) {
589
                                $years = (int) ($row->age / self::DAYS_IN_YEAR);
590
                                $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
591
                            }
592
                        }
593
                        break;
594
                    case self::Z_AXIS_TIME:
595
                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
596
                        $z_axis         = $this->axisYears($boundaries_csv);
597
                        // The stats query doesn't have an "all" function, so query M/F separately
598
                        foreach (['M', 'F'] as $sex) {
599
                            $prev_boundary = 0;
600
                            foreach (array_keys($z_axis) as $boundary) {
601
                                $rows = $statistics->statsMarrAgeQuery($sex, $prev_boundary, $boundary);
602
                                foreach ($rows as $row) {
603
                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
604
                                    $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
605
                                }
606
                                $prev_boundary = $boundary + 1;
607
                            }
608
                        }
609
                        break;
610
                    default:
611
                        throw new HttpNotFoundException();
612
                }
613
614
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
615
616
            case self::X_AXIS_AGE_AT_FIRST_MARRIAGE:
617
                $chart_title    = I18N::translate('Age in year of first marriage');
618
                $x_axis_title   = I18N::translate('age');
619
                $boundaries_csv = Validator::parsedBody($request)->string('x-axis-boundaries-ages_m');
620
                $x_axis         = $this->axisNumbers($boundaries_csv);
621
622
                switch ($y_axis_type) {
623
                    case self::Y_AXIS_NUMBERS:
624
                        $y_axis_title = I18N::translate('Individuals');
625
                        break;
626
                    case self::Y_AXIS_PERCENT:
627
                        $y_axis_title = '%';
628
                        break;
629
                    default:
630
                        throw new HttpNotFoundException();
631
                }
632
633
                switch ($z_axis_type) {
634
                    case self::Z_AXIS_ALL:
635
                        $z_axis = $this->axisAll();
636
                        // The stats query doesn't have an "all" function, so query M/F separately
637
                        foreach (['M', 'F'] as $sex) {
638
                            $rows = $statistics->statsMarrAgeQuery($sex);
639
                            $indi = [];
640
                            foreach ($rows as $row) {
641
                                if (!in_array($row->d_gid, $indi, true)) {
642
                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
643
                                    $this->fillYData($years, 0, 1, $x_axis, $z_axis, $ydata);
644
                                    $indi[] = $row->d_gid;
645
                                }
646
                            }
647
                        }
648
                        break;
649
                    case self::Z_AXIS_SEX:
650
                        $z_axis = $this->axisSexes();
651
                        foreach (array_keys($z_axis) as $sex) {
652
                            $rows = $statistics->statsMarrAgeQuery($sex);
653
                            $indi = [];
654
                            foreach ($rows as $row) {
655
                                if (!in_array($row->d_gid, $indi, true)) {
656
                                    $years = (int) ($row->age / self::DAYS_IN_YEAR);
657
                                    $this->fillYData($years, $sex, 1, $x_axis, $z_axis, $ydata);
658
                                    $indi[] = $row->d_gid;
659
                                }
660
                            }
661
                        }
662
                        break;
663
                    case self::Z_AXIS_TIME:
664
                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
665
                        $z_axis         = $this->axisYears($boundaries_csv);
666
                        // The stats query doesn't have an "all" function, so query M/F separately
667
                        foreach (['M', 'F'] as $sex) {
668
                            $prev_boundary = 0;
669
                            $indi          = [];
670
                            foreach (array_keys($z_axis) as $boundary) {
671
                                $rows = $statistics->statsMarrAgeQuery($sex, $prev_boundary, $boundary);
672
                                foreach ($rows as $row) {
673
                                    if (!in_array($row->d_gid, $indi, true)) {
674
                                        $years = (int) ($row->age / self::DAYS_IN_YEAR);
675
                                        $this->fillYData($years, $boundary, 1, $x_axis, $z_axis, $ydata);
676
                                        $indi[] = $row->d_gid;
677
                                    }
678
                                }
679
                                $prev_boundary = $boundary + 1;
680
                            }
681
                        }
682
                        break;
683
                    default:
684
                        throw new HttpNotFoundException();
685
                }
686
687
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
688
689
            case self::X_AXIS_NUMBER_OF_CHILDREN:
690
                $chart_title  = I18N::translate('Number of children');
691
                $x_axis_title = I18N::translate('Children');
692
                $x_axis       = $this->axisNumbers('0,1,2,3,4,5,6,7,8,9,10');
693
694
                switch ($y_axis_type) {
695
                    case self::Y_AXIS_NUMBERS:
696
                        $y_axis_title = I18N::translate('Families');
697
                        break;
698
                    case self::Y_AXIS_PERCENT:
699
                        $y_axis_title = '%';
700
                        break;
701
                    default:
702
                        throw new HttpNotFoundException();
703
                }
704
705
                switch ($z_axis_type) {
706
                    case self::Z_AXIS_ALL:
707
                        $z_axis = $this->axisAll();
708
                        $rows   = $statistics->statsChildrenQuery();
709
                        foreach ($rows as $row) {
710
                            $this->fillYData($row->f_numchil, 0, $row->total, $x_axis, $z_axis, $ydata);
711
                        }
712
                        break;
713
                    case self::Z_AXIS_TIME:
714
                        $boundaries_csv = Validator::parsedBody($request)->string('z-axis-boundaries-periods');
715
                        $z_axis         = $this->axisYears($boundaries_csv);
716
                        $prev_boundary  = 0;
717
                        foreach (array_keys($z_axis) as $boundary) {
718
                            $rows = $statistics->statsChildrenQuery($prev_boundary, $boundary);
719
                            foreach ($rows as $row) {
720
                                $this->fillYData($row->f_numchil, $boundary, $row->total, $x_axis, $z_axis, $ydata);
721
                            }
722
                            $prev_boundary = $boundary + 1;
723
                        }
724
                        break;
725
                    default:
726
                        throw new HttpNotFoundException();
727
                }
728
729
                return response($this->myPlot($chart_title, $x_axis, $x_axis_title, $ydata, $y_axis_title, $z_axis, $y_axis_type));
730
731
            default:
732
                throw new HttpNotFoundException();
733
        }
734
    }
735
736
    /**
737
     * @return array<string>
738
     */
739
    private function axisAll(): array
740
    {
741
        return [
742
            I18N::translate('Total'),
743
        ];
744
    }
745
746
    /**
747
     * @return array<string>
748
     */
749
    private function axisSexes(): array
750
    {
751
        return [
752
            'M' => I18N::translate('Male'),
753
            'F' => I18N::translate('Female'),
754
        ];
755
    }
756
757
    /**
758
     * Labels for the X axis
759
     *
760
     * @return array<string>
761
     */
762
    private function axisMonths(): array
763
    {
764
        return [
765
            'JAN' => I18N::translateContext('NOMINATIVE', 'January'),
766
            'FEB' => I18N::translateContext('NOMINATIVE', 'February'),
767
            'MAR' => I18N::translateContext('NOMINATIVE', 'March'),
768
            'APR' => I18N::translateContext('NOMINATIVE', 'April'),
769
            'MAY' => I18N::translateContext('NOMINATIVE', 'May'),
770
            'JUN' => I18N::translateContext('NOMINATIVE', 'June'),
771
            'JUL' => I18N::translateContext('NOMINATIVE', 'July'),
772
            'AUG' => I18N::translateContext('NOMINATIVE', 'August'),
773
            'SEP' => I18N::translateContext('NOMINATIVE', 'September'),
774
            'OCT' => I18N::translateContext('NOMINATIVE', 'October'),
775
            'NOV' => I18N::translateContext('NOMINATIVE', 'November'),
776
            'DEC' => I18N::translateContext('NOMINATIVE', 'December'),
777
        ];
778
    }
779
780
    /**
781
     * Convert a list of N year-boundaries into N+1 year-ranges for the z-axis.
782
     *
783
     * @return array<string>
784
     */
785
    private function axisYears(string $boundaries_csv): array
786
    {
787
        $boundaries = explode(',', $boundaries_csv);
788
        $boundaries = array_map(static fn (string $x): int => (int) $x, $boundaries);
789
790
        $axis = [];
791
        foreach ($boundaries as $n => $boundary) {
792
            if ($n === 0) {
793
                $axis[$boundary - 1] = '–' . I18N::digits($boundary);
794
            } else {
795
                $axis[$boundary - 1] = I18N::digits($boundaries[$n - 1]) . '–' . I18N::digits($boundary);
796
            }
797
        }
798
799
        $axis[PHP_INT_MAX] = I18N::digits($boundaries[array_key_last($boundaries)]) . '–';
800
801
        return $axis;
802
    }
803
804
    /**
805
     * Create the X axis.
806
     *
807
     * @return array<string>
808
     */
809
    private function axisNumbers(string $boundaries_csv): array
810
    {
811
        $boundaries = explode(',', $boundaries_csv);
812
        $boundaries = array_map(static fn (string $x): int => (int) $x, $boundaries);
813
814
        $axis = [];
815
        foreach ($boundaries as $n => $boundary) {
816
            if ($n === 0) {
817
                $prev_boundary = 0;
818
            } else {
819
                $prev_boundary = $boundaries[$n - 1] + 1;
820
            }
821
822
            if ($prev_boundary === $boundary) {
823
                /* I18N: A range of numbers */
824
                $axis[$boundary] = I18N::number($boundary);
825
            } else {
826
                /* I18N: A range of numbers */
827
                $axis[$boundary] = I18N::translate('%1$s–%2$s', I18N::number($prev_boundary), I18N::number($boundary));
828
            }
829
        }
830
831
        /* I18N: Label on a graph; 40+ means 40 or more */
832
        $axis[PHP_INT_MAX] = I18N::translate('%s+', I18N::number($boundaries[array_key_last($boundaries)]));
833
834
        return $axis;
835
    }
836
837
    /**
838
     * Calculate the Y axis.
839
     *
840
     * @param int|string        $x
841
     * @param int|string        $z
842
     * @param int|string        $value
843
     * @param array<string>     $x_axis
844
     * @param array<string>     $z_axis
845
     * @param array<array<int>> $ydata
846
     */
847
    private function fillYData($x, $z, $value, array $x_axis, array $z_axis, array &$ydata): void
848
    {
849
        $x = $this->findAxisEntry($x, $x_axis);
850
        $z = $this->findAxisEntry($z, $z_axis);
851
852
        $ydata[$z][$x] ??= 0;
853
        $ydata[$z][$x] += $value;
854
    }
855
856
    /**
857
     * Find the axis entry for a given value.
858
     * Some are direct lookup (e.g. M/F, JAN/FEB/MAR).
859
     * Others need to find the appropriate range.
860
     *
861
     * @param int|string    $value
862
     * @param array<string> $axis
863
     *
864
     * @return int|string
865
     */
866
    private function findAxisEntry($value, array $axis)
867
    {
868
        if (is_numeric($value)) {
869
            $value = (int) $value;
870
871
            if (!array_key_exists($value, $axis)) {
872
                foreach (array_keys($axis) as $boundary) {
873
                    if ($value <= $boundary) {
874
                        $value = $boundary;
875
                        break;
876
                    }
877
                }
878
            }
879
        }
880
881
        return $value;
882
    }
883
884
    /**
885
     * Plot the data.
886
     *
887
     * @param array<string>     $x_axis
888
     * @param array<array<int>> $ydata
889
     * @param array<string>     $z_axis
890
     */
891
    private function myPlot(
892
        string $chart_title,
893
        array $x_axis,
894
        string $x_axis_title,
895
        array $ydata,
896
        string $y_axis_title,
897
        array $z_axis,
898
        int $y_axis_type
899
    ): string {
900
        if ($ydata === []) {
901
            return I18N::translate('This information is not available.');
902
        }
903
904
        // Colors for z-axis
905
        $colors = [];
906
        $index  = 0;
907
        while (count($colors) < count($ydata)) {
908
            $colors[] = self::Z_AXIS_COLORS[$index];
909
            $index    = ($index + 1) % count(self::Z_AXIS_COLORS);
910
        }
911
912
        // Convert our sparse dataset into a fixed-size array
913
        $tmp = [];
914
        foreach (array_keys($z_axis) as $z) {
915
            foreach (array_keys($x_axis) as $x) {
916
                $tmp[$z][$x] = $ydata[$z][$x] ?? 0;
917
            }
918
        }
919
        $ydata = $tmp;
920
921
        // Convert the chart data to percentage
922
        if ($y_axis_type === self::Y_AXIS_PERCENT) {
923
            // Normalise each (non-zero!) set of data to total 100%
924
            array_walk($ydata, static function (array &$x) {
925
                $sum = array_sum($x);
926
                if ($sum > 0) {
927
                    $x = array_map(static fn (float $y): float => $y * 100.0 / $sum, $x);
928
                }
929
            });
930
        }
931
932
        $data = [
933
            array_merge(
934
                [I18N::translate('Century')],
935
                array_values($z_axis)
936
            ),
937
        ];
938
939
        $intermediate = [];
940
        foreach ($ydata as $months) {
941
            foreach ($months as $month => $value) {
942
                $intermediate[$month][] = [
943
                    'v' => $value,
944
                    'f' => $y_axis_type === self::Y_AXIS_PERCENT ? sprintf('%.1f%%', $value) : $value,
945
                ];
946
            }
947
        }
948
949
        foreach ($intermediate as $key => $values) {
950
            $data[] = array_merge(
951
                [$x_axis[$key]],
952
                $values
953
            );
954
        }
955
956
        $chart_options = [
957
            'title'    => '',
958
            'subtitle' => '',
959
            'height'   => 400,
960
            'width'    => '100%',
961
            'legend'   => [
962
                'position'  => count($z_axis) > 1 ? 'right' : 'none',
963
                'alignment' => 'center',
964
            ],
965
            'tooltip'  => [
966
                'format' => '\'%\'',
967
            ],
968
            'vAxis'    => [
969
                'title' => $y_axis_title,
970
            ],
971
            'hAxis'    => [
972
                'title' => $x_axis_title,
973
            ],
974
            'colors'   => $colors,
975
        ];
976
977
        return view('statistics/other/charts/custom', [
978
            'data'          => $data,
979
            'chart_options' => $chart_options,
980
            'chart_title'   => $chart_title,
981
            'language'      => I18N::languageTag(),
982
        ]);
983
    }
984
}
985